merge: Upstream & New Changes

Pulled in latest changes
Changed renote to boost
Added Sponsors
Added limit for renote/boost now you can only boost once.
This commit is contained in:
Marie 2023-10-14 03:41:32 +02:00 committed by GitHub
commit 2d78ec4940
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
220 changed files with 6181 additions and 2813 deletions

View file

@ -95,6 +95,14 @@ redis:
# #prefix: example-prefix # #prefix: example-prefix
# #db: 1 # #db: 1
#redisForTimelines:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌───────────────────────────┐ # ┌───────────────────────────┐
#───┘ MeiliSearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────

View file

@ -105,6 +105,16 @@ redis:
# # You can specify more ioredis options... # # You can specify more ioredis options...
# #username: example-username # #username: example-username
#redisForTimelines:
# host: localhost
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# # You can specify more ioredis options...
# #username: example-username
# ┌───────────────────────────┐ # ┌───────────────────────────┐
#───┘ MeiliSearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────

View file

@ -4,7 +4,9 @@
"service": "app", "service": "app",
"workspaceFolder": "/workspace", "workspaceFolder": "/workspace",
"features": { "features": {
"ghcr.io/devcontainers-contrib/features/pnpm:2": {}, "ghcr.io/devcontainers-contrib/features/pnpm:2": {
"version": "8.8.0"
},
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {
"version": "20.5.1" "version": "20.5.1"
} }

View file

@ -95,6 +95,14 @@ redis:
# #prefix: example-prefix # #prefix: example-prefix
# #db: 1 # #db: 1
#redisForTimelines:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌───────────────────────────┐ # ┌───────────────────────────┐
#───┘ MeiliSearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────

View file

@ -12,12 +12,76 @@
--> -->
## 2023.10.0 ## 2023.x.x (unreleased)
### General
-
### Client ### Client
- Enhance: TLの返信表示オプションを記憶するように
### Server
-
## 2023.10.1
### General
- Enhance: ローカルタイムライン、ソーシャルタイムラインで返信を含むかどうか設定可能に
### Client
- Fix: 絵文字ピッカーで横に長いカスタム絵文字が見切れる問題を修正
### Server
- Fix: フォローしているユーザーからの自分の投稿への返信がタイムラインに含まれない問題を修正
- Fix: users/notesでセンシティブチャンネルの投稿が含まれる場合がある問題を修正
## 2023.10.0
### NOTE
- 2023.9.2で導入されたノート編集機能はクオリティの高い実装が困難であることが判明したため撤回されました
- アップデートを行うと、タイムラインが一時的にリセットされます
- ソフトミュート設定はクライアントではなくサーバー側に保存されるようになったため、アップデートを行うとソフトミュートの設定がリセットされます
### Changes
- API: users/notes, notes/local-timeline で fileType 指定はできなくなりました
- API: notes/featured でページネーションは他APIと同様 untilId を使って行うようになりました
### General
- Feat: ユーザーごとに他ユーザーへの返信をタイムラインに含めるか設定可能になりました
- Feat: ユーザーリスト内のメンバーごとに他ユーザーへの返信をユーザーリストタイムラインに含めるか設定可能になりました
- Feat: ユーザーごとのハイライト
- Feat: プライバシーポリシー・運営者情報Impressumの指定が可能になりました
- プライバシーポリシーはサーバー登録時に同意確認が入ります
- Feat: タイムラインがリアルタイム更新中に広告を挿入できるようになりました
- デフォルトは無効
- 頻度はコントロールパネルから設定できます。運営中のサーバーのTLの流速を見て、最適な値を指定してください。
- Enhance: ソフトワードミュートとハードワードミュートは統合されました
- Enhance: モデレーションログ機能の強化
- Enhance: ローカリゼーションの更新
- Enhance: 依存関係の更新
- Fix: ダイレクト投稿をリノートできてしまう問題を修正
- Fix: ユーザーリストTLにチャンネル投稿が含まれる問題を修正
### Client
- Feat: 「ファイルの詳細」ページを追加
- ドライブのファイルの拡大プレビューができるように
- ファイルが添付されたノートの一覧が表示できるように
- Enhance: 二要素認証のバックアップコード一覧をテキストファイルでダウンロード可能に - Enhance: 二要素認証のバックアップコード一覧をテキストファイルでダウンロード可能に
- Enhance: 動画再生時のデフォルトボリュームを30%に
- Fix: リアクションしたユーザ一覧のUIが稀に左上に残ってしまう不具合を修正 - Fix: リアクションしたユーザ一覧のUIが稀に左上に残ってしまう不具合を修正
### Server
- Enhance: drive/files/attached-notes がページネーションに対応しました
- Enhance: タイムライン取得時のパフォーマンスを大幅に向上
- Enhance: ハイライト取得時のパフォーマンスを大幅に向上
- Enhance: トレンドハッシュタグ取得時のパフォーマンスを大幅に向上
- Enhance: WebSocket接続が多い場合のパフォーマンスを向上
- Enhance: 不要なPostgreSQLのインデックスを削除しパフォーマンスを向上
- Fix: 連合なしアンケートに投票をするとUpdateがリモートに配信されてしまうのを修正
- Fix: nodeinfoにおいてCORS用のヘッダーが設定されていないのを修正
- Fix: 同じ種類のTLのストリーミングを複数接続できない問題を修正
- Fix: アンテナTLを途中までしかページネーションできなくなることがある問題を修正
- Fix: 「ファイル付きのみ」のTLでファイル無しの新着ートが流れる問題を修正
- Fix: プロセスが終了しない、あるいは非常に時間がかかる問題を修正
## 2023.9.3 ## 2023.9.3
### General ### General
- Enhance: ノートの翻訳機能の利用可否をロールで設定可能に - Enhance: ノートの翻訳機能の利用可否をロールで設定可能に

View file

@ -116,6 +116,14 @@ redis:
# #prefix: example-prefix # #prefix: example-prefix
# #db: 1 # #db: 1
#redisForTimelines:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌───────────────────────────┐ # ┌───────────────────────────┐
#───┘ MeiliSearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────

View file

@ -1184,11 +1184,6 @@ _wordMute:
muteWords: "الكلمات المحظورة" muteWords: "الكلمات المحظورة"
muteWordsDescription: "افصل بينهم بمسافة لاستخدام معامل \"و\" أو بسطر لاستخدام معامل \"أو\"." muteWordsDescription: "افصل بينهم بمسافة لاستخدام معامل \"و\" أو بسطر لاستخدام معامل \"أو\"."
muteWordsDescription2: "احصر الكلمات المفتاحية بين بين شرطتين مائلتين لاستخدامها كتعابير نمطية" muteWordsDescription2: "احصر الكلمات المفتاحية بين بين شرطتين مائلتين لاستخدامها كتعابير نمطية"
softDescription: "اخف الملاحظات التي تستوف الشروط من الخيط الزمني."
hardDescription: "اخف الملاحظات التي تستوف الشروط من الخيط الزمني.بالإضافة إلى أن هذه الملاحظات ستبقى مخفية حتى وإن تغيرت الشروط."
soft: "لينة"
hard: "قاسية"
mutedNotes: "الملاحظات المكتومة"
_instanceMute: _instanceMute:
instanceMuteDescription: "هذه سيحجب كل ملاحظات الخوادم المحجوبة ومشاركاتها والردود على تلك الملاحظات حتى وإن كانت من خادم غير محجوب." instanceMuteDescription: "هذه سيحجب كل ملاحظات الخوادم المحجوبة ومشاركاتها والردود على تلك الملاحظات حتى وإن كانت من خادم غير محجوب."
instanceMuteDescription2: "مدخلة لكل سطر" instanceMuteDescription2: "مدخلة لكل سطر"
@ -1248,8 +1243,6 @@ _sfx:
note: "الملاحظات" note: "الملاحظات"
noteMy: "ملاحظتي" noteMy: "ملاحظتي"
notification: "الإشعارات" notification: "الإشعارات"
chat: "المحادثة"
chatBg: "المحادثة (الخلفية)"
antenna: "الهوائيات" antenna: "الهوائيات"
channel: "إشعارات القنات" channel: "إشعارات القنات"
_ago: _ago:

View file

@ -932,11 +932,6 @@ _wordMute:
muteWords: "নিঃশব্দ করা শব্দগুলি" muteWords: "নিঃশব্দ করা শব্দগুলি"
muteWordsDescription: "স্পেস দিয়ে আলাদা করলে AND শর্ত তৈরি হবে এবং আলাদা লাইনে লিখলে OR শর্ত তৈরি হবে।" muteWordsDescription: "স্পেস দিয়ে আলাদা করলে AND শর্ত তৈরি হবে এবং আলাদা লাইনে লিখলে OR শর্ত তৈরি হবে।"
muteWordsDescription2: "রেগুলার এক্সপ্রেশন ব্যবহার করতে স্ল্যাশ দিয়ে কীওয়ার্ডকে ঘিরে রাখুন।" muteWordsDescription2: "রেগুলার এক্সপ্রেশন ব্যবহার করতে স্ল্যাশ দিয়ে কীওয়ার্ডকে ঘিরে রাখুন।"
softDescription: "টাইমলাইন থেকে নির্দিষ্ট শর্তানুযায়ী নোট লুকিয়ে রাখে।"
hardDescription: "নির্দিষ্ট শর্তানুযায়ী নোটগুলিকে টাইমলাইন থেকে বাদ দেয়। আপনি শর্ত পরিবর্তন করলেও যে নোটগুলি যোগ করা হয়নি সেগুলি বাদ দেওয়া হবে।"
soft: "নমনীয়"
hard: "কঠোর"
mutedNotes: "মিউট করা নোটগুলি"
_instanceMute: _instanceMute:
instanceMuteDescription: "কনফিগার করা ইন্সট্যান্সের সব নোট এবং রিনোট মিউট করুন, মিউট করা ইন্সট্যান্সের ব্যবহারকারীদের উত্তর সহ।" instanceMuteDescription: "কনফিগার করা ইন্সট্যান্সের সব নোট এবং রিনোট মিউট করুন, মিউট করা ইন্সট্যান্সের ব্যবহারকারীদের উত্তর সহ।"
instanceMuteDescription2: "প্রতিটিকে আলাদা লাইনে লিখুন" instanceMuteDescription2: "প্রতিটিকে আলাদা লাইনে লিখুন"
@ -1000,9 +995,6 @@ _theme:
infoFg: "তথ্যের পাঠ্য" infoFg: "তথ্যের পাঠ্য"
infoWarnBg: "ওয়ার্নিং এর পটভূমি" infoWarnBg: "ওয়ার্নিং এর পটভূমি"
infoWarnFg: "ওয়ার্নিং এর পাঠ্য" infoWarnFg: "ওয়ার্নিং এর পাঠ্য"
cwBg: "CW বাটনের পটভূমি"
cwFg: "CW বাটনের পাঠ্য"
cwHoverBg: "CW বাটনের পটভূমি (হভার)"
toastBg: "বিজ্ঞপ্তির পটভূমি" toastBg: "বিজ্ঞপ্তির পটভূমি"
toastFg: "বিজ্ঞপ্তির পাঠ্য" toastFg: "বিজ্ঞপ্তির পাঠ্য"
buttonBg: "বাটনের পটভূমি" buttonBg: "বাটনের পটভূমি"
@ -1020,8 +1012,6 @@ _sfx:
note: "নোটগুলি" note: "নোটগুলি"
noteMy: "নোট (আপনার)" noteMy: "নোট (আপনার)"
notification: "বিজ্ঞপ্তি" notification: "বিজ্ঞপ্তি"
chat: "চ্যাট"
chatBg: "চ্যাট (ব্যাকগ্রাউন্ড)"
antenna: "অ্যান্টেনাগুলি" antenna: "অ্যান্টেনাগুলি"
channel: "চ্যানেলের বিজ্ঞপ্তি" channel: "চ্যানেলের বিজ্ঞপ্তি"
_ago: _ago:

View file

@ -398,7 +398,6 @@ _theme:
_sfx: _sfx:
note: "Notes" note: "Notes"
notification: "Notificacions" notification: "Notificacions"
chat: "Xat"
antenna: "Antenes" antenna: "Antenes"
_2fa: _2fa:
renewTOTPCancel: "No, gràcies" renewTOTPCancel: "No, gràcies"

View file

@ -1559,11 +1559,6 @@ _wordMute:
muteWords: "Ztlumená slova" muteWords: "Ztlumená slova"
muteWordsDescription: "Podmínku AND oddělujte mezerami, podmínku OR oddělujte řádkovými zlomy." muteWordsDescription: "Podmínku AND oddělujte mezerami, podmínku OR oddělujte řádkovými zlomy."
muteWordsDescription2: "Chcete-li použít regulární výrazy, obklopte klíčová slova lomítky." muteWordsDescription2: "Chcete-li použít regulární výrazy, obklopte klíčová slova lomítky."
softDescription: "Skrýt poznámky, které splňují nastavené podmínky, z časové osy."
hardDescription: "Zabrání přidání poznámek splňujících nastavené podmínky na časovou osu. Kromě toho nebudou tyto poznámky přidány na časovou osu, ani když se podmínky změní."
soft: "Měkký"
hard: "Tvrdý"
mutedNotes: "Ztlumené poznámky"
_instanceMute: _instanceMute:
instanceMuteDescription: "Tímhle se ztlumí všechny poznámky/poznámky z uvedených instancí, včetně poznámek uživatelů, kteří odpovídají uživateli ze ztlumené instance." instanceMuteDescription: "Tímhle se ztlumí všechny poznámky/poznámky z uvedených instancí, včetně poznámek uživatelů, kteří odpovídají uživateli ze ztlumené instance."
instanceMuteDescription2: "Oddělte novými řádky" instanceMuteDescription2: "Oddělte novými řádky"
@ -1627,9 +1622,6 @@ _theme:
infoFg: "Text informací" infoFg: "Text informací"
infoWarnBg: "Pozadí varování" infoWarnBg: "Pozadí varování"
infoWarnFg: "Text varování" infoWarnFg: "Text varování"
cwBg: "Pozadí CW tlačítka"
cwFg: "Text CW tlačítka"
cwHoverBg: "Pozadí CW tlačítka (Hover)"
toastBg: "Pozadí oznámení" toastBg: "Pozadí oznámení"
toastFg: "Text oznámení" toastFg: "Text oznámení"
buttonBg: "Pozadí tlačítka" buttonBg: "Pozadí tlačítka"
@ -1647,8 +1639,6 @@ _sfx:
note: "Poznámky" note: "Poznámky"
noteMy: "Moje poznámka" noteMy: "Moje poznámka"
notification: "Oznámení" notification: "Oznámení"
chat: "Zprávy"
chatBg: "Chat (Pozadí)"
antenna: "Antény" antenna: "Antény"
channel: "Oznámení kanálu" channel: "Oznámení kanálu"
_ago: _ago:

View file

@ -15,7 +15,7 @@ gotIt: "Verstanden!"
cancel: "Abbrechen" cancel: "Abbrechen"
noThankYou: "Nein, danke" noThankYou: "Nein, danke"
enterUsername: "Benutzername eingeben" enterUsername: "Benutzername eingeben"
renotedBy: "Renote von {user}" renotedBy: "Geboostet von {user}"
noNotes: "Keine Notizen gefunden" noNotes: "Keine Notizen gefunden"
noNotifications: "Keine Benachrichtigungen gefunden" noNotifications: "Keine Benachrichtigungen gefunden"
instance: "Instanz" instance: "Instanz"
@ -105,13 +105,13 @@ followRequests: "Follow-Anfragen"
unfollow: "Entfolgen" unfollow: "Entfolgen"
followRequestPending: "Follow-Anfrage ausstehend" followRequestPending: "Follow-Anfrage ausstehend"
enterEmoji: "Gib ein Emoji ein" enterEmoji: "Gib ein Emoji ein"
renote: "Renote" renote: "Boost"
unrenote: "Renote zurücknehmen" unrenote: "Boost zurücknehmen"
renoted: "Renote getätigt." renoted: "Boost getätigt."
cantRenote: "Renote dieses Beitrags nicht möglich." cantRenote: "Boosten dieses Beitrags nicht möglich."
cantReRenote: "Renote einer Renote nicht möglich." cantReRenote: "Boosten von einen Boost nicht möglich."
quote: "Zitieren" quote: "Zitieren"
inChannelRenote: "Kanal-interner Renote" inChannelRenote: "Kanal-interner Boost"
inChannelQuote: "Kanal-internes Zitat" inChannelQuote: "Kanal-internes Zitat"
pinnedNote: "Angeheftete Notiz" pinnedNote: "Angeheftete Notiz"
pinned: "Angeheftet" pinned: "Angeheftet"
@ -694,7 +694,7 @@ notesCount: "Anzahl der Notizen"
repliesCount: "Anzahl gesendeter Antworten" repliesCount: "Anzahl gesendeter Antworten"
renotesCount: "Anzahl getätigter Renotes" renotesCount: "Anzahl getätigter Renotes"
repliedCount: "Anzahl erhaltener Antworten" repliedCount: "Anzahl erhaltener Antworten"
renotedCount: "Anzahl erhaltener Renotes" renotedCount: "Anzahl erhaltener Boosts"
followingCount: "Anzahl gefolgter Benutzer" followingCount: "Anzahl gefolgter Benutzer"
followersCount: "Anzahl an Followern" followersCount: "Anzahl an Followern"
sentReactionsCount: "Anzahl gesendeter Reaktionen" sentReactionsCount: "Anzahl gesendeter Reaktionen"
@ -1126,6 +1126,15 @@ edited: "Bearbeitet"
notificationRecieveConfig: "Benachrichtigungseinstellungen" notificationRecieveConfig: "Benachrichtigungseinstellungen"
mutualFollow: "Gegenseitig gefolgt" mutualFollow: "Gegenseitig gefolgt"
fileAttachedOnly: "Nur Notizen mit Dateien" fileAttachedOnly: "Nur Notizen mit Dateien"
showRepliesToOthersInTimeline: "Antworten in Chronik anzeigen"
hideRepliesToOthersInTimeline: "Antworten nicht in Chronik anzeigen"
externalServices: "Externe Dienste"
impressum: "Impressum"
impressumUrl: "Impressums-URL"
impressumDescription: "In manchen Ländern, wie Deutschland und dessen Umgebung, ist die Angabe von Betreiberinformationen (ein Impressum) bei kommerziellem Betrieb zwingend."
privacyPolicy: "Datenschutzerklärung"
privacyPolicyUrl: "Datenschutzerklärungs-URL"
tosAndPrivacyPolicy: "Nutzungsbedingungen und Datenschutzerklärung"
_announcement: _announcement:
forExistingUsers: "Nur für existierende Nutzer" forExistingUsers: "Nur für existierende Nutzer"
forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt." forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt."
@ -1456,7 +1465,6 @@ _role:
gtlAvailable: "Kann auf die globale Chronik zugreifen" gtlAvailable: "Kann auf die globale Chronik zugreifen"
ltlAvailable: "Kann auf die lokale Chronik zugreifen" ltlAvailable: "Kann auf die lokale Chronik zugreifen"
canPublicNote: "Kann öffentliche Notizen erstellen" canPublicNote: "Kann öffentliche Notizen erstellen"
canEditNote: "Notizbearbeitung"
canInvite: "Erstellung von Einladungscodes für diese Instanz" canInvite: "Erstellung von Einladungscodes für diese Instanz"
inviteLimit: "Maximalanzahl an Einladungen" inviteLimit: "Maximalanzahl an Einladungen"
inviteLimitCycle: "Zyklus des Einladungslimits" inviteLimitCycle: "Zyklus des Einladungslimits"
@ -1476,6 +1484,7 @@ _role:
descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver." descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver."
canHideAds: "Kann Werbung ausblenden" canHideAds: "Kann Werbung ausblenden"
canSearchNotes: "Nutzung der Notizsuchfunktion" canSearchNotes: "Nutzung der Notizsuchfunktion"
canUseTranslator: "Verwendung des Übersetzers"
_condition: _condition:
isLocal: "Lokaler Benutzer" isLocal: "Lokaler Benutzer"
isRemote: "Benutzer fremder Instanz" isRemote: "Benutzer fremder Instanz"
@ -1524,6 +1533,10 @@ _ad:
reduceFrequencyOfThisAd: "Diese Werbung weniger anzeigen" reduceFrequencyOfThisAd: "Diese Werbung weniger anzeigen"
hide: "Ausblenden" hide: "Ausblenden"
timezoneinfo: "Der Wochentag wird durch die Serverzeitzone bestimmt." timezoneinfo: "Der Wochentag wird durch die Serverzeitzone bestimmt."
adsSettings: "Werbeeinstellungen"
notesPerOneAd: "Werbeintervall während Echtzeitaktualisierung (Notizen pro Werbung)"
setZeroToDisable: "Setze dies auf 0, um Werbung während Echtzeitaktualisierung zu deaktivieren"
adsTooClose: "Durch den momentan sehr niedrigen Werbeintervall kann es zu einer starken Verschlechterung der Benutzererfahrung kommen."
_forgotPassword: _forgotPassword:
enterEmail: "Gib die Email-Adresse ein, mit der du dich registriert hast. An diese wird ein Link gesendet, mit dem du dein Passwort zurücksetzen kannst." enterEmail: "Gib die Email-Adresse ein, mit der du dich registriert hast. An diese wird ein Link gesendet, mit dem du dein Passwort zurücksetzen kannst."
ifNoEmail: "Solltest du bei der Registrierung keine Email-Adresse angegeben haben, wende dich bitte an den Administrator." ifNoEmail: "Solltest du bei der Registrierung keine Email-Adresse angegeben haben, wende dich bitte an den Administrator."
@ -1609,11 +1622,6 @@ _wordMute:
muteWords: "Stummgeschaltete Wörter" muteWords: "Stummgeschaltete Wörter"
muteWordsDescription: "Zum Nutzen einer \"UND\"-Verknüpfung Einträge mit Leerzeichen trennen, zum Nutzen einer \"ODER\"-Verknüpfung Einträge mit einem Zeilenumbruch trennen." muteWordsDescription: "Zum Nutzen einer \"UND\"-Verknüpfung Einträge mit Leerzeichen trennen, zum Nutzen einer \"ODER\"-Verknüpfung Einträge mit einem Zeilenumbruch trennen."
muteWordsDescription2: "Umgib Schlüsselworter mit Schrägstrichen, um Reguläre Ausdrücke zu verwenden." muteWordsDescription2: "Umgib Schlüsselworter mit Schrägstrichen, um Reguläre Ausdrücke zu verwenden."
softDescription: "Notizen, die die angegebenen Konditionen erfüllen, in der Chronik ausblenden."
hardDescription: "Verhindern, dass Notizen, die die angegebenen Konditionen erfüllen, der Chronik hinzugefügt werden. Zudem werden diese Notizen auch nicht der Chronik hinzugefügt, falls die Konditionen geändert werden."
soft: "Leicht"
hard: "Schwer"
mutedNotes: "Stummgeschaltete Notizen"
_instanceMute: _instanceMute:
instanceMuteDescription: "Schaltet alle Notizen/Renotes stumm, die von den gelisteten Instanzen stammen, inklusive Antworten von Benutzern an einen Benutzer einer stummgeschalteten Instanz." instanceMuteDescription: "Schaltet alle Notizen/Renotes stumm, die von den gelisteten Instanzen stammen, inklusive Antworten von Benutzern an einen Benutzer einer stummgeschalteten Instanz."
instanceMuteDescription2: "Instanzen getrennt durch Zeilenumbrüchen angeben" instanceMuteDescription2: "Instanzen getrennt durch Zeilenumbrüchen angeben"
@ -1677,9 +1685,6 @@ _theme:
infoFg: "Text von Informationen" infoFg: "Text von Informationen"
infoWarnBg: "Hintergrund von Warnungen" infoWarnBg: "Hintergrund von Warnungen"
infoWarnFg: "Text von Warnungen" infoWarnFg: "Text von Warnungen"
cwBg: "Hintergrund des Inhaltswarnungsknopfs"
cwFg: "Text des Inhaltswarnungsknopfs"
cwHoverBg: "Hintergrund des Inhaltswarnungsknopfs (Mouseover)"
toastBg: "Hintergrund von Benachrichtigungen" toastBg: "Hintergrund von Benachrichtigungen"
toastFg: "Text von Benachrichtigungen" toastFg: "Text von Benachrichtigungen"
buttonBg: "Hintergrund von Schaltflächen" buttonBg: "Hintergrund von Schaltflächen"
@ -1697,8 +1702,6 @@ _sfx:
note: "Notizen" note: "Notizen"
noteMy: "Meine Notizen" noteMy: "Meine Notizen"
notification: "Benachrichtigungen" notification: "Benachrichtigungen"
chat: "Chat"
chatBg: "Chat (Hintergrund)"
antenna: "Antennen" antenna: "Antennen"
channel: "Kanalbenachrichtigung" channel: "Kanalbenachrichtigung"
_ago: _ago:
@ -2022,7 +2025,7 @@ _notification:
youGotMention: "{name} hat dich erwähnt" youGotMention: "{name} hat dich erwähnt"
youGotReply: "{name} hat dir geantwortet" youGotReply: "{name} hat dir geantwortet"
youGotQuote: "{name} hat dich zitiert" youGotQuote: "{name} hat dich zitiert"
youRenoted: "Renote deiner Notiz von {name}" youRenoted: "Boost deiner Notiz von {name}"
youWereFollowed: "ist dir gefolgt" youWereFollowed: "ist dir gefolgt"
youReceivedFollowRequest: "Du hast eine Follow-Anfrage erhalten" youReceivedFollowRequest: "Du hast eine Follow-Anfrage erhalten"
yourFollowRequestAccepted: "Deine Follow-Anfrage wurde akzeptiert" yourFollowRequestAccepted: "Deine Follow-Anfrage wurde akzeptiert"
@ -2138,3 +2141,11 @@ _moderationLogTypes:
createAd: "Werbung erstellt" createAd: "Werbung erstellt"
deleteAd: "Werbung gelöscht" deleteAd: "Werbung gelöscht"
updateAd: "Werbung aktualisiert" updateAd: "Werbung aktualisiert"
_fileViewer:
title: "Dateiinformationen"
type: "Dateityp"
size: "Dateigröße"
url: "URL"
uploadedAt: "Hochgeladen am"
attachedNotes: "Zugehörige Notizen"
thisPageCanBeSeenFromTheAuthor: "Nur der Benutzer, der diese Datei hochgeladen hat, kann diese Seite sehen."

View file

@ -303,8 +303,6 @@ _theme:
_sfx: _sfx:
note: "Σημειώματα" note: "Σημειώματα"
notification: "Ειδοποιήσεις" notification: "Ειδοποιήσεις"
chat: "Συνομιλία"
chatBg: "Συνομιλία (Παρασκήνιο)"
antenna: "Αντένες" antenna: "Αντένες"
channel: "Ειδοποιήσεις καναλιών" channel: "Ειδοποιήσεις καναλιών"
_ago: _ago:

View file

@ -15,7 +15,7 @@ gotIt: "Got it!"
cancel: "Cancel" cancel: "Cancel"
noThankYou: "Not now" noThankYou: "Not now"
enterUsername: "Enter username" enterUsername: "Enter username"
renotedBy: "Renoted by {user}" renotedBy: "Boosted by {user}"
noNotes: "No notes" noNotes: "No notes"
noNotifications: "No notifications" noNotifications: "No notifications"
instance: "Instance" instance: "Instance"
@ -105,11 +105,11 @@ followRequests: "Follow requests"
unfollow: "Unfollow" unfollow: "Unfollow"
followRequestPending: "Follow request pending" followRequestPending: "Follow request pending"
enterEmoji: "Enter an emoji" enterEmoji: "Enter an emoji"
renote: "Renote" renote: "Boost"
unrenote: "Remove renote" unrenote: "Remove boost"
renoted: "Renoted." renoted: "Boosted."
cantRenote: "This post can't be renoted." cantRenote: "This post can't be boosted."
cantReRenote: "A renote can't be renoted." cantReRenote: "A boost can't be boosted."
quote: "Quote" quote: "Quote"
inChannelRenote: "Channel-only Renote" inChannelRenote: "Channel-only Renote"
inChannelQuote: "Channel-only Quote" inChannelQuote: "Channel-only Quote"
@ -696,7 +696,7 @@ notesCount: "Number of notes"
repliesCount: "Number of replies sent" repliesCount: "Number of replies sent"
renotesCount: "Number of renotes sent" renotesCount: "Number of renotes sent"
repliedCount: "Number of replies received" repliedCount: "Number of replies received"
renotedCount: "Number of renotes received" renotedCount: "Number of boosts received"
followingCount: "Number of followed accounts" followingCount: "Number of followed accounts"
followersCount: "Number of followers" followersCount: "Number of followers"
sentReactionsCount: "Number of sent reactions" sentReactionsCount: "Number of sent reactions"
@ -1128,6 +1128,15 @@ edited: "Edited"
notificationRecieveConfig: "Notification Settings" notificationRecieveConfig: "Notification Settings"
mutualFollow: "Mutual follow" mutualFollow: "Mutual follow"
fileAttachedOnly: "Only notes with files" fileAttachedOnly: "Only notes with files"
showRepliesToOthersInTimeline: "Show replies to others in TL"
hideRepliesToOthersInTimeline: "Hide replies to others from TL"
externalServices: "External Services"
impressum: "Impressum"
impressumUrl: "Impressum URL"
impressumDescription: "In some countries, like germany, the inclusion of operator contact information (an Impressum) is legally required for commercial websites."
privacyPolicy: "Privacy Policy"
privacyPolicyUrl: "Privacy Policy URL"
tosAndPrivacyPolicy: "Terms of Service and Privacy Policy"
_announcement: _announcement:
forExistingUsers: "Existing users only" forExistingUsers: "Existing users only"
forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it." forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it."
@ -1458,7 +1467,6 @@ _role:
gtlAvailable: "Can view the global timeline" gtlAvailable: "Can view the global timeline"
ltlAvailable: "Can view the local timeline" ltlAvailable: "Can view the local timeline"
canPublicNote: "Can send public notes" canPublicNote: "Can send public notes"
canEditNote: "Note editing"
canInvite: "Can create instance invite codes" canInvite: "Can create instance invite codes"
inviteLimit: "Invite limit" inviteLimit: "Invite limit"
inviteLimitCycle: "Invite limit cooldown" inviteLimitCycle: "Invite limit cooldown"
@ -1478,6 +1486,7 @@ _role:
descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. " descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. "
canHideAds: "Can hide ads" canHideAds: "Can hide ads"
canSearchNotes: "Usage of note search" canSearchNotes: "Usage of note search"
canUseTranslator: "Translator usage"
_condition: _condition:
isLocal: "Local user" isLocal: "Local user"
isRemote: "Remote user" isRemote: "Remote user"
@ -1526,6 +1535,10 @@ _ad:
reduceFrequencyOfThisAd: "Show this ad less" reduceFrequencyOfThisAd: "Show this ad less"
hide: "Hide" hide: "Hide"
timezoneinfo: "The day of the week is determined from the server's timezone." timezoneinfo: "The day of the week is determined from the server's timezone."
adsSettings: "Ad settings"
notesPerOneAd: "Real-time update ad placement interval (Notes per ad)"
setZeroToDisable: "Set this value to 0 to disable real-time update ads"
adsTooClose: "The current ad interval may significantly worsen the user experience due to being too low."
_forgotPassword: _forgotPassword:
enterEmail: "Enter the email address you used to register. A link with which you can reset your password will then be sent to it." enterEmail: "Enter the email address you used to register. A link with which you can reset your password will then be sent to it."
ifNoEmail: "If you did not use an email during registration, please contact the instance administrator instead." ifNoEmail: "If you did not use an email during registration, please contact the instance administrator instead."
@ -1611,11 +1624,6 @@ _wordMute:
muteWords: "Muted words" muteWords: "Muted words"
muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition." muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition."
muteWordsDescription2: "Surround keywords with slashes to use regular expressions." muteWordsDescription2: "Surround keywords with slashes to use regular expressions."
softDescription: "Hide notes that fulfil the set conditions from the timeline."
hardDescription: "Prevents notes fulfilling the set conditions from being added to the timeline. In addition, these notes will not be added to the timeline even if the conditions are changed."
soft: "Soft"
hard: "Hard"
mutedNotes: "Muted notes"
_instanceMute: _instanceMute:
instanceMuteDescription: "This will mute any notes/renotes from the listed instances, including those of users replying to a user from a muted instance." instanceMuteDescription: "This will mute any notes/renotes from the listed instances, including those of users replying to a user from a muted instance."
instanceMuteDescription2: "Separate with newlines" instanceMuteDescription2: "Separate with newlines"
@ -1679,9 +1687,6 @@ _theme:
infoFg: "Information text" infoFg: "Information text"
infoWarnBg: "Warning background" infoWarnBg: "Warning background"
infoWarnFg: "Warning text" infoWarnFg: "Warning text"
cwBg: "CW button background"
cwFg: "CW button text"
cwHoverBg: "CW button background (Hover)"
toastBg: "Notification background" toastBg: "Notification background"
toastFg: "Notification text" toastFg: "Notification text"
buttonBg: "Button background" buttonBg: "Button background"
@ -1699,8 +1704,6 @@ _sfx:
note: "New note" note: "New note"
noteMy: "Own note" noteMy: "Own note"
notification: "Notifications" notification: "Notifications"
chat: "Chat"
chatBg: "Chat (Background)"
antenna: "Antennas" antenna: "Antennas"
channel: "Channel notifications" channel: "Channel notifications"
_ago: _ago:
@ -2024,7 +2027,7 @@ _notification:
youGotMention: "{name} mentioned you" youGotMention: "{name} mentioned you"
youGotReply: "{name} replied to you" youGotReply: "{name} replied to you"
youGotQuote: "{name} quoted you" youGotQuote: "{name} quoted you"
youRenoted: "Renote from {name}" youRenoted: "Boost from {name}"
youWereFollowed: "followed you" youWereFollowed: "followed you"
youReceivedFollowRequest: "You've received a follow request" youReceivedFollowRequest: "You've received a follow request"
yourFollowRequestAccepted: "Your follow request was accepted" yourFollowRequestAccepted: "Your follow request was accepted"
@ -2106,7 +2109,7 @@ _webhookSettings:
followed: "When being followed" followed: "When being followed"
note: "When posting a note" note: "When posting a note"
reply: "When receiving a reply" reply: "When receiving a reply"
renote: "When renoted" renote: "When boosted"
reaction: "When receiving a reaction" reaction: "When receiving a reaction"
mention: "When being mentioned" mention: "When being mentioned"
_moderationLogTypes: _moderationLogTypes:
@ -2213,3 +2216,11 @@ _mfm:
backgroundDescription: "Change the background color of text." backgroundDescription: "Change the background color of text."
plain: "Plain" plain: "Plain"
plainDescription: "Deactivates the effects of all MFM contained within this MFM effect." plainDescription: "Deactivates the effects of all MFM contained within this MFM effect."
_fileViewer:
title: "File details"
type: "File type"
size: "Filesize"
url: "URL"
uploadedAt: "Uploaded at"
attachedNotes: "Attached notes"
thisPageCanBeSeenFromTheAuthor: "This page can only be seen by the user who uploaded this file."

View file

@ -1603,11 +1603,6 @@ _wordMute:
muteWords: "Palabras que silenciar" muteWords: "Palabras que silenciar"
muteWordsDescription: "Separar con espacios indica una declaracion And, separar con lineas nuevas indica una declaracion Or。" muteWordsDescription: "Separar con espacios indica una declaracion And, separar con lineas nuevas indica una declaracion Or。"
muteWordsDescription2: "Encerrar las palabras clave entre numerales para usar expresiones regulares" muteWordsDescription2: "Encerrar las palabras clave entre numerales para usar expresiones regulares"
softDescription: "Ocultar en la linea de tiempo las notas que cumplen las condiciones"
hardDescription: "Evitar que se agreguen a la linea de tiempo las notas que cumplen las condiciones. Las notas no agregadas seguirán quitadas aunque cambien las condiciones."
soft: "Suave"
hard: "Duro"
mutedNotes: "Notas silenciadas"
_instanceMute: _instanceMute:
instanceMuteDescription: "Silencia todas las notas y reposts de la instancias seleccionadas, incluyendo respuestas a los usuarios de las mismas" instanceMuteDescription: "Silencia todas las notas y reposts de la instancias seleccionadas, incluyendo respuestas a los usuarios de las mismas"
instanceMuteDescription2: "Separar por líneas" instanceMuteDescription2: "Separar por líneas"
@ -1671,9 +1666,6 @@ _theme:
infoFg: "Texto de información" infoFg: "Texto de información"
infoWarnBg: "Fondo de advertencias" infoWarnBg: "Fondo de advertencias"
infoWarnFg: "Texto de advertencias" infoWarnFg: "Texto de advertencias"
cwBg: "Fondo del botón CW"
cwFg: "Texto del botón CW"
cwHoverBg: "Fondo del botón CW (hover)"
toastBg: "Fondo de notificaciones" toastBg: "Fondo de notificaciones"
toastFg: "Texto de notificaciones" toastFg: "Texto de notificaciones"
buttonBg: "Fondo de botón" buttonBg: "Fondo de botón"
@ -1691,8 +1683,6 @@ _sfx:
note: "Notas" note: "Notas"
noteMy: "Nota (a mí mismo)" noteMy: "Nota (a mí mismo)"
notification: "Notificaciones" notification: "Notificaciones"
chat: "Chat"
chatBg: "Chat (Fondo)"
antenna: "Antena receptora" antenna: "Antena receptora"
channel: "Notificaciones del canal" channel: "Notificaciones del canal"
_ago: _ago:

View file

@ -45,6 +45,7 @@ pin: "Épingler sur le profil"
unpin: "Désépingler" unpin: "Désépingler"
copyContent: "Copier le contenu" copyContent: "Copier le contenu"
copyLink: "Copier le lien" copyLink: "Copier le lien"
copyLinkRenote: "Copier le lien de la renote"
delete: "Supprimer" delete: "Supprimer"
deleteAndEdit: "Supprimer et réécrire" deleteAndEdit: "Supprimer et réécrire"
deleteAndEditConfirm: "Êtes-vous sûr de vouloir effacer cette note et la modifier ? Vous perdrez toutes les réactions, renotes et réponses." deleteAndEditConfirm: "Êtes-vous sûr de vouloir effacer cette note et la modifier ? Vous perdrez toutes les réactions, renotes et réponses."
@ -129,6 +130,8 @@ unmarkAsSensitive: "Supprimer le marquage comme sensible"
enterFileName: "Entrer le nom du fichier" enterFileName: "Entrer le nom du fichier"
mute: "Masquer" mute: "Masquer"
unmute: "Ne plus masquer" unmute: "Ne plus masquer"
renoteMute: "Masquer les renotes"
renoteUnmute: "Ne plus masquer les renotes"
block: "Bloquer" block: "Bloquer"
unblock: "Débloquer" unblock: "Débloquer"
suspend: "Suspendre" suspend: "Suspendre"
@ -414,6 +417,7 @@ moderator: "Modérateur·rice·s"
moderation: "Modérations" moderation: "Modérations"
moderationNote: "Note de modération" moderationNote: "Note de modération"
addModerationNote: "Ajouter une note de modération" addModerationNote: "Ajouter une note de modération"
moderationLogs: "Journal de modération"
nUsersMentioned: "{n} utilisateur·rice·s mentionné·e·s" nUsersMentioned: "{n} utilisateur·rice·s mentionné·e·s"
securityKeyAndPasskey: "Sécurité et clés de sécurité" securityKeyAndPasskey: "Sécurité et clés de sécurité"
securityKey: "Clé de sécurité" securityKey: "Clé de sécurité"
@ -472,6 +476,7 @@ aboutX: "À propos de {x}"
emojiStyle: "Style des émojis" emojiStyle: "Style des émojis"
native: "Natif" native: "Natif"
disableDrawer: "Les menus ne s'affichent pas dans le tiroir" disableDrawer: "Les menus ne s'affichent pas dans le tiroir"
showNoteActionsOnlyHover: "Afficher les actions de note uniquement au survol"
noHistory: "Pas d'historique" noHistory: "Pas d'historique"
signinHistory: "Historique de connexion" signinHistory: "Historique de connexion"
enableAdvancedMfm: "Activer la MFM avancée" enableAdvancedMfm: "Activer la MFM avancée"
@ -647,6 +652,7 @@ behavior: "Comportement"
sample: "Exemple" sample: "Exemple"
abuseReports: "Signalements" abuseReports: "Signalements"
reportAbuse: "Signaler" reportAbuse: "Signaler"
reportAbuseRenote: "Signaler la renote"
reportAbuseOf: "Signaler {name}" reportAbuseOf: "Signaler {name}"
fillAbuseReportDescription: "Veuillez expliquer les raisons du signalement. S'il s'agit d'une note précise, veuillez en donner le lien." fillAbuseReportDescription: "Veuillez expliquer les raisons du signalement. S'il s'agit d'une note précise, veuillez en donner le lien."
abuseReported: "Le rapport est envoyé. Merci." abuseReported: "Le rapport est envoyé. Merci."
@ -671,6 +677,8 @@ clip: "Clip"
createNew: "Créer nouveau" createNew: "Créer nouveau"
optional: "Facultatif" optional: "Facultatif"
createNewClip: "Créer un nouveau clip" createNewClip: "Créer un nouveau clip"
unclip: "Supprimer le clip"
confirmToUnclipAlreadyClippedNote: "Cette note fait déjà partie du clip « {name} ». Souhaitez-vous la supprimer de ce clip ?"
public: "Public" public: "Public"
private: "Privé" private: "Privé"
i18nInfo: "Misskey est traduit dans différentes langues par des bénévoles. Vous pouvez contribuer à {link}." i18nInfo: "Misskey est traduit dans différentes langues par des bénévoles. Vous pouvez contribuer à {link}."
@ -933,12 +941,15 @@ unsubscribePushNotification: "Désactiver les notifications push"
pushNotificationAlreadySubscribed: "Les notifications push sont déjà activées" pushNotificationAlreadySubscribed: "Les notifications push sont déjà activées"
pushNotificationNotSupported: "Votre navigateur ou votre instance ne prend pas en charge les notifications push" pushNotificationNotSupported: "Votre navigateur ou votre instance ne prend pas en charge les notifications push"
sendPushNotificationReadMessage: "Supprimer les notifications push une fois que les notifications ou messages pertinents ont été lus." sendPushNotificationReadMessage: "Supprimer les notifications push une fois que les notifications ou messages pertinents ont été lus."
windowMaximize: "Maximiser"
windowMinimize: "Minimaliser"
windowRestore: "Restaurer" windowRestore: "Restaurer"
caption: "Libellé" caption: "Libellé"
loggedInAsBot: "Connecté actuellement en tant que bot" loggedInAsBot: "Connecté actuellement en tant que bot"
tools: "Outils" tools: "Outils"
cannotLoad: "Chargement impossible" cannotLoad: "Chargement impossible"
like: "J'aime" like: "J'aime"
unlike: "Ne plus aimer"
numberOfLikes: "Favoris" numberOfLikes: "Favoris"
show: "Affichage" show: "Affichage"
neverShow: "Ne plus afficher" neverShow: "Ne plus afficher"
@ -949,6 +960,7 @@ noRole: "Aucun rôle"
normalUser: "Simple utilisateur·rice" normalUser: "Simple utilisateur·rice"
undefined: "Non défini" undefined: "Non défini"
assign: "Attribuer" assign: "Attribuer"
unassign: "Retirer"
color: "Couleur" color: "Couleur"
manageCustomEmojis: "Gestion des émojis personnalisés" manageCustomEmojis: "Gestion des émojis personnalisés"
preset: "Préréglage" preset: "Préréglage"
@ -958,12 +970,16 @@ thisPostMayBeAnnoying: "Cette note peut gêner d'autres personnes."
thisPostMayBeAnnoyingHome: "Publier vers le fil principal" thisPostMayBeAnnoyingHome: "Publier vers le fil principal"
thisPostMayBeAnnoyingCancel: "Annuler" thisPostMayBeAnnoyingCancel: "Annuler"
thisPostMayBeAnnoyingIgnore: "Publier quand-même" thisPostMayBeAnnoyingIgnore: "Publier quand-même"
collapseRenotes: "Réduire les renotes déjà vues"
internalServerError: "Erreur interne du serveur" internalServerError: "Erreur interne du serveur"
copyErrorInfo: "Copier les détails de lerreur" copyErrorInfo: "Copier les détails de lerreur"
exploreOtherServers: "Trouver une autre instance" exploreOtherServers: "Trouver une autre instance"
disableFederationOk: "Désactiver" disableFederationOk: "Désactiver"
likeOnly: "Les favoris uniquement" likeOnly: "Les favoris uniquement"
sensitiveWords: "Mots sensibles"
notesSearchNotAvailable: "La recherche de notes n'est pas disponible."
license: "Licence" license: "Licence"
myClips: "Mes clips"
video: "Vidéo" video: "Vidéo"
videos: "Vidéos" videos: "Vidéos"
dataSaver: "Économiseur de données" dataSaver: "Économiseur de données"
@ -973,6 +989,7 @@ accountMovedShort: "Ce compte a migré"
operationForbidden: "Opération non autorisée" operationForbidden: "Opération non autorisée"
addMemo: "Ajouter un mémo" addMemo: "Ajouter un mémo"
reactionsList: "Réactions" reactionsList: "Réactions"
renotesList: "Liste de renotes"
notificationDisplay: "Style des notifications" notificationDisplay: "Style des notifications"
leftTop: "En haut à gauche" leftTop: "En haut à gauche"
rightTop: "En haut à droite" rightTop: "En haut à droite"
@ -982,6 +999,7 @@ vertical: "Vertical"
horizontal: "Latéral" horizontal: "Latéral"
serverRules: "Règles du serveur" serverRules: "Règles du serveur"
archive: "Archive" archive: "Archive"
displayOfNote: "Affichage de la note"
youFollowing: "Abonné·e" youFollowing: "Abonné·e"
options: "Options" options: "Options"
later: "Plus tard" later: "Plus tard"
@ -1001,6 +1019,7 @@ pinnedList: "Liste épinglée"
notifyNotes: "Notifier à propos des nouvelles notes" notifyNotes: "Notifier à propos des nouvelles notes"
authentication: "Authentification" authentication: "Authentification"
authenticationRequiredToContinue: "Veuillez vous authentifier pour continuer" authenticationRequiredToContinue: "Veuillez vous authentifier pour continuer"
showRenotes: "Afficher les renotes"
_announcement: _announcement:
readConfirmTitle: "Marquer comme lu ?" readConfirmTitle: "Marquer comme lu ?"
_initialAccountSetting: _initialAccountSetting:
@ -1082,12 +1101,20 @@ _achievements:
title: "Beaucoup d'amis" title: "Beaucoup d'amis"
_followers10: _followers10:
title: "Abonnez-moi !" title: "Abonnez-moi !"
description: "Obtenir plus de 10 abonné·e·s"
_followers50:
description: "Obtenir plus de 50 abonné·e·s"
_followers100: _followers100:
title: "Populaire" title: "Populaire"
description: "Obtenir plus de 100 abonné·e·s"
_followers300:
description: "Obtenir plus de 300 abonné·e·s"
_followers500: _followers500:
title: "Tour radio" title: "Tour radio"
description: "Obtenir plus de 500 abonné·e·s"
_followers1000: _followers1000:
title: "Influenceur·euse" title: "Influenceur·euse"
description: "Obtenir plus de 1000 abonné·e·s"
_iLoveMisskey: _iLoveMisskey:
title: "Jadore Misskey" title: "Jadore Misskey"
description: "Publication « J❤ #Misskey »" description: "Publication « J❤ #Misskey »"
@ -1151,6 +1178,7 @@ _role:
high: "Haute" high: "Haute"
_options: _options:
canManageCustomEmojis: "Gestion des émojis personnalisés" canManageCustomEmojis: "Gestion des émojis personnalisés"
wordMuteMax: "Nombre maximal de caractères dans le filtre de mots"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "L'apprentissage automatique peut être utilisé pour détecter automatiquement les médias sensibles à modérer. La sollicitation des serveurs augmente légèrement." description: "L'apprentissage automatique peut être utilisé pour détecter automatiquement les médias sensibles à modérer. La sollicitation des serveurs augmente légèrement."
sensitivity: "Sensibilité de la détection" sensitivity: "Sensibilité de la détection"
@ -1267,11 +1295,6 @@ _wordMute:
muteWords: "Mots à filtrer" muteWords: "Mots à filtrer"
muteWordsDescription: "Séparer avec des espaces pour la condition AND. Séparer avec un saut de ligne pour une condition OR." muteWordsDescription: "Séparer avec des espaces pour la condition AND. Séparer avec un saut de ligne pour une condition OR."
muteWordsDescription2: "Pour utiliser des expressions régulières (regex), mettez les mots-clés entre barres obliques." muteWordsDescription2: "Pour utiliser des expressions régulières (regex), mettez les mots-clés entre barres obliques."
softDescription: "Masquez les notes de votre fil selon les paramètres que vous définissez."
hardDescription: "Empêchez votre fil de charger les notes selon les paramètres que vous définissez. Cette action est irréversible : si vous modifiez ces paramètres plus tard, les notes précédemment filtrées ne seront pas récupérées."
soft: "Doux"
hard: "Strict"
mutedNotes: "Notes filtrées"
_instanceMute: _instanceMute:
instanceMuteDescription: "Met en sourdine toutes les notes et renotes de l'instance configurée, y compris les réponses aux utilisateurs de l'instance muette." instanceMuteDescription: "Met en sourdine toutes les notes et renotes de l'instance configurée, y compris les réponses aux utilisateurs de l'instance muette."
instanceMuteDescription2: "Séparer avec de nouvelles lignes" instanceMuteDescription2: "Séparer avec de nouvelles lignes"
@ -1335,9 +1358,6 @@ _theme:
infoFg: "Texte d'information" infoFg: "Texte d'information"
infoWarnBg: "Arrière-plan des avertissements" infoWarnBg: "Arrière-plan des avertissements"
infoWarnFg: "Texte davertissement" infoWarnFg: "Texte davertissement"
cwBg: "Arrière-plan du CW"
cwFg: "Texte du bouton CW"
cwHoverBg: "Arrière-plan du bouton CW (survolé)"
toastBg: "Arrière-plan de la bulle de notification" toastBg: "Arrière-plan de la bulle de notification"
toastFg: "Texte de la bulle de notification" toastFg: "Texte de la bulle de notification"
buttonBg: "Arrière-plan du bouton" buttonBg: "Arrière-plan du bouton"
@ -1355,8 +1375,6 @@ _sfx:
note: "Nouvelle note" note: "Nouvelle note"
noteMy: "Ma note" noteMy: "Ma note"
notification: "Notifications" notification: "Notifications"
chat: "Discuter"
chatBg: "Discussion (arrière-plan)"
antenna: "Réception de lantenne" antenna: "Réception de lantenne"
channel: "Notifications de canal" channel: "Notifications de canal"
_ago: _ago:

View file

@ -1564,11 +1564,6 @@ _wordMute:
muteWords: "Kata yang dibisukan" muteWords: "Kata yang dibisukan"
muteWordsDescription: "Pisahkan dengan spasi untuk kondisi AND. Pisahkan dengan baris baru untuk kondisi OR." muteWordsDescription: "Pisahkan dengan spasi untuk kondisi AND. Pisahkan dengan baris baru untuk kondisi OR."
muteWordsDescription2: "Kurung kata kunci dengan garis miring untuk menggunakan ekspresi reguler." muteWordsDescription2: "Kurung kata kunci dengan garis miring untuk menggunakan ekspresi reguler."
softDescription: "Sembunyikan catatan yang memenuhi aturan kondisi dari lini masa."
hardDescription: "Cegah catatan memenuhi aturan kondisi dari ditambahkan ke lini masa. Dengan tambahan, catatan berikut tidak akan ditambahkan ke lini masa meskipun jika kondisi tersebut diubah."
soft: "Lembut"
hard: "Keras"
mutedNotes: "Catatan yang dibisukan"
_instanceMute: _instanceMute:
instanceMuteDescription: "Pengaturan ini akan membisukan note/renote apa saja dari instansi yang terdaftar, termasuk pengguna yang membalas pengguna lain dalam instansi yang dibisukan." instanceMuteDescription: "Pengaturan ini akan membisukan note/renote apa saja dari instansi yang terdaftar, termasuk pengguna yang membalas pengguna lain dalam instansi yang dibisukan."
instanceMuteDescription2: "Pisah dengan baris baru" instanceMuteDescription2: "Pisah dengan baris baru"
@ -1632,9 +1627,6 @@ _theme:
infoFg: "Teks informasi" infoFg: "Teks informasi"
infoWarnBg: "Latar belakang peringatan" infoWarnBg: "Latar belakang peringatan"
infoWarnFg: "Teks peringatan" infoWarnFg: "Teks peringatan"
cwBg: "Latar belakang tombol Sembunyikan Konten"
cwFg: "Teks tombol Sembunyikan Konten"
cwHoverBg: "Latar belakang tombol Sembunyikan Konten (Mengambang)"
toastBg: "Latar belakang notifikasi" toastBg: "Latar belakang notifikasi"
toastFg: "Teks notifikasi" toastFg: "Teks notifikasi"
buttonBg: "Latar belakang tombol" buttonBg: "Latar belakang tombol"
@ -1652,8 +1644,6 @@ _sfx:
note: "Catatan" note: "Catatan"
noteMy: "Catatan (Saya)" noteMy: "Catatan (Saya)"
notification: "Notifikasi" notification: "Notifikasi"
chat: "Pesan"
chatBg: "Obrolan (Latar Belakang)"
antenna: "Penerimaan Antenna" antenna: "Penerimaan Antenna"
channel: "Notifikasi Kanal" channel: "Notifikasi Kanal"
_ago: _ago:

35
locales/index.d.ts vendored
View file

@ -167,6 +167,8 @@ export interface Locale {
"flagAsBotDescription": string; "flagAsBotDescription": string;
"flagAsCat": string; "flagAsCat": string;
"flagAsCatDescription": string; "flagAsCatDescription": string;
"flagSpeakAsCat": string;
"flagSpeakAsCatDescription": string;
"flagShowTimelineReplies": string; "flagShowTimelineReplies": string;
"flagShowTimelineRepliesDescription": string; "flagShowTimelineRepliesDescription": string;
"autoAcceptFollowed": string; "autoAcceptFollowed": string;
@ -1129,6 +1131,15 @@ export interface Locale {
"notificationRecieveConfig": string; "notificationRecieveConfig": string;
"mutualFollow": string; "mutualFollow": string;
"fileAttachedOnly": string; "fileAttachedOnly": string;
"showRepliesToOthersInTimeline": string;
"hideRepliesToOthersInTimeline": string;
"externalServices": string;
"impressum": string;
"impressumUrl": string;
"impressumDescription": string;
"privacyPolicy": string;
"privacyPolicyUrl": string;
"tosAndPrivacyPolicy": string;
"_announcement": { "_announcement": {
"forExistingUsers": string; "forExistingUsers": string;
"forExistingUsersDescription": string; "forExistingUsersDescription": string;
@ -1542,7 +1553,6 @@ export interface Locale {
"gtlAvailable": string; "gtlAvailable": string;
"ltlAvailable": string; "ltlAvailable": string;
"canPublicNote": string; "canPublicNote": string;
"canEditNote": string;
"canInvite": string; "canInvite": string;
"inviteLimit": string; "inviteLimit": string;
"inviteLimitCycle": string; "inviteLimitCycle": string;
@ -1619,6 +1629,10 @@ export interface Locale {
"reduceFrequencyOfThisAd": string; "reduceFrequencyOfThisAd": string;
"hide": string; "hide": string;
"timezoneinfo": string; "timezoneinfo": string;
"adsSettings": string;
"notesPerOneAd": string;
"setZeroToDisable": string;
"adsTooClose": string;
}; };
"_forgotPassword": { "_forgotPassword": {
"enterEmail": string; "enterEmail": string;
@ -1719,11 +1733,6 @@ export interface Locale {
"muteWords": string; "muteWords": string;
"muteWordsDescription": string; "muteWordsDescription": string;
"muteWordsDescription2": string; "muteWordsDescription2": string;
"softDescription": string;
"hardDescription": string;
"soft": string;
"hard": string;
"mutedNotes": string;
}; };
"_instanceMute": { "_instanceMute": {
"instanceMuteDescription": string; "instanceMuteDescription": string;
@ -1789,9 +1798,6 @@ export interface Locale {
"infoFg": string; "infoFg": string;
"infoWarnBg": string; "infoWarnBg": string;
"infoWarnFg": string; "infoWarnFg": string;
"cwBg": string;
"cwFg": string;
"cwHoverBg": string;
"toastBg": string; "toastBg": string;
"toastFg": string; "toastFg": string;
"buttonBg": string; "buttonBg": string;
@ -1811,8 +1817,6 @@ export interface Locale {
"note": string; "note": string;
"noteMy": string; "noteMy": string;
"notification": string; "notification": string;
"chat": string;
"chatBg": string;
"antenna": string; "antenna": string;
"channel": string; "channel": string;
}; };
@ -2289,6 +2293,15 @@ export interface Locale {
"deleteAd": string; "deleteAd": string;
"updateAd": string; "updateAd": string;
}; };
"_fileViewer": {
"title": string;
"type": string;
"size": string;
"url": string;
"uploadedAt": string;
"attachedNotes": string;
"thisPageCanBeSeenFromTheAuthor": string;
};
} }
declare const locales: { declare const locales: {
[lang: string]: Locale; [lang: string]: Locale;

View file

@ -64,7 +64,7 @@ reply: "Rispondi"
loadMore: "Mostra di più" loadMore: "Mostra di più"
showMore: "Espandi" showMore: "Espandi"
showLess: "Comprimi" showLess: "Comprimi"
youGotNewFollower: "Ti sta seguendo" youGotNewFollower: "Adesso ti segue"
receiveFollowRequest: "Hai ricevuto una richiesta di follow" receiveFollowRequest: "Hai ricevuto una richiesta di follow"
followRequestAccepted: "Ha accettato la tua richiesta di follow" followRequestAccepted: "Ha accettato la tua richiesta di follow"
mention: "Menzioni" mention: "Menzioni"
@ -78,7 +78,7 @@ download: "Scarica"
driveFileDeleteConfirm: "Vuoi davvero eliminare il file \"{name}\", e le Note a cui è stato allegato?" driveFileDeleteConfirm: "Vuoi davvero eliminare il file \"{name}\", e le Note a cui è stato allegato?"
unfollowConfirm: "Vuoi davvero smettere di seguire {name}?" unfollowConfirm: "Vuoi davvero smettere di seguire {name}?"
exportRequested: "Hai richiesto un'esportazione, e potrebbe volerci tempo. Quando sarà compiuta, il file verrà aggiunto direttamente al Drive." exportRequested: "Hai richiesto un'esportazione, e potrebbe volerci tempo. Quando sarà compiuta, il file verrà aggiunto direttamente al Drive."
importRequested: "Hai richiesto un'importazione. Può volerci tempo. " importRequested: "Hai richiesto un'importazione. Potrebbe richiedere un po' di tempo."
lists: "Liste" lists: "Liste"
noLists: "Nessuna lista" noLists: "Nessuna lista"
note: "Nota" note: "Nota"
@ -113,7 +113,7 @@ cantReRenote: "È impossibile rinotare una Rinota."
quote: "Cita" quote: "Cita"
inChannelRenote: "Rinota nel canale" inChannelRenote: "Rinota nel canale"
inChannelQuote: "Cita nel canale" inChannelQuote: "Cita nel canale"
pinnedNote: "Nota fissata" pinnedNote: "Nota in primo piano"
pinned: "Fissa sul profilo" pinned: "Fissa sul profilo"
you: "Tu" you: "Tu"
clickToShow: "Clicca per visualizzare" clickToShow: "Clicca per visualizzare"
@ -186,7 +186,7 @@ recipient: "Destinatario"
annotation: "Annotazione preventiva" annotation: "Annotazione preventiva"
federation: "Federazione" federation: "Federazione"
instances: "Istanza" instances: "Istanza"
registeredAt: "Registrato presso" registeredAt: "Prima federazione"
latestRequestReceivedAt: "Ultima richiesta ricevuta" latestRequestReceivedAt: "Ultima richiesta ricevuta"
latestStatus: "Ultimo stato" latestStatus: "Ultimo stato"
storageUsage: "Capienza dei dischi" storageUsage: "Capienza dei dischi"
@ -336,7 +336,7 @@ instanceName: "Nome dell'istanza"
instanceDescription: "Descrizione dell'istanza" instanceDescription: "Descrizione dell'istanza"
maintainerName: "Nome dell'amministratore" maintainerName: "Nome dell'amministratore"
maintainerEmail: "Indirizzo e-mail dell'amministratore" maintainerEmail: "Indirizzo e-mail dell'amministratore"
tosUrl: "URL dei termini del servizio e della privacy" tosUrl: "URL delle condizioni d'uso"
thisYear: "Anno" thisYear: "Anno"
thisMonth: "Mese" thisMonth: "Mese"
today: "Oggi" today: "Oggi"
@ -364,7 +364,7 @@ pinnedUsersDescription: "Elenca gli/le utenti che vuoi fissare in cima alla pagi
pinnedPages: "Pagine in evidenza" pinnedPages: "Pagine in evidenza"
pinnedPagesDescription: "Specifica il percorso delle pagine che vuoi fissare in cima alla pagina dell'istanza. Una pagina per riga." pinnedPagesDescription: "Specifica il percorso delle pagine che vuoi fissare in cima alla pagina dell'istanza. Una pagina per riga."
pinnedClipId: "ID della Clip in evidenza" pinnedClipId: "ID della Clip in evidenza"
pinnedNotes: "Nota fissata" pinnedNotes: "Note in primo piano"
hcaptcha: "hCaptcha" hcaptcha: "hCaptcha"
enableHcaptcha: "Abilita hCaptcha" enableHcaptcha: "Abilita hCaptcha"
hcaptchaSiteKey: "Chiave del sito" hcaptchaSiteKey: "Chiave del sito"
@ -384,7 +384,7 @@ name: "Nome"
antennaSource: "Fonte dell'antenna" antennaSource: "Fonte dell'antenna"
antennaKeywords: "Parole chiavi da ricevere" antennaKeywords: "Parole chiavi da ricevere"
antennaExcludeKeywords: "Parole chiavi da escludere" antennaExcludeKeywords: "Parole chiavi da escludere"
antennaKeywordsDescription: "Separare con uno spazio indica la condizione \"E\". Separare con un'interruzzione riga indica la condizione \"O\"." antennaKeywordsDescription: "Sparando con uno spazio indichi la condizione E (and). Separando con un a capo, indichi la condizione O (or)."
notifyAntenna: "Invia notifiche delle nuove note" notifyAntenna: "Invia notifiche delle nuove note"
withFileAntenna: "Solo note con file in allegato" withFileAntenna: "Solo note con file in allegato"
enableServiceworker: "Abilita ServiceWorker" enableServiceworker: "Abilita ServiceWorker"
@ -393,7 +393,7 @@ caseSensitive: "Sensibile alla distinzione tra maiuscole e minuscole"
withReplies: "Includere le risposte" withReplies: "Includere le risposte"
connectedTo: "Connessione ai seguenti profili:" connectedTo: "Connessione ai seguenti profili:"
notesAndReplies: "Note e risposte" notesAndReplies: "Note e risposte"
withFiles: "Con file in allegato" withFiles: "Con allegati"
silence: "Silenzia" silence: "Silenzia"
silenceConfirm: "Vuoi davvero silenziare questo profilo?" silenceConfirm: "Vuoi davvero silenziare questo profilo?"
unsilence: "Riattiva" unsilence: "Riattiva"
@ -461,7 +461,7 @@ invitationCode: "Codice di invito"
checking: "Confermando" checking: "Confermando"
available: "Disponibile" available: "Disponibile"
unavailable: "Il nome utente è già in uso" unavailable: "Il nome utente è già in uso"
usernameInvalidFormat: "Il nome utente può contenere solo lettere, numeri e '_'" usernameInvalidFormat: "Il nome utente deve avere solo caratteri alfanumerici e trattino basso '_'"
tooShort: "Troppo breve" tooShort: "Troppo breve"
tooLong: "Troppo lungo" tooLong: "Troppo lungo"
weakPassword: "Password debole" weakPassword: "Password debole"
@ -1121,7 +1121,20 @@ unnotifyNotes: "Interrompi le notifiche di nuove Note"
authentication: "Autenticazione" authentication: "Autenticazione"
authenticationRequiredToContinue: "Per procedere, è richiesta l'autenticazione" authenticationRequiredToContinue: "Per procedere, è richiesta l'autenticazione"
dateAndTime: "Data e Ora" dateAndTime: "Data e Ora"
showRenotes: "Leggi le Rinota" showRenotes: "Includi le Rinota"
edited: "Modificato"
notificationRecieveConfig: "Preferenze di notifica"
mutualFollow: "Follow reciproco"
fileAttachedOnly: "Solo con allegati"
showRepliesToOthersInTimeline: "Risposte altrui nella TL"
hideRepliesToOthersInTimeline: "Nascondi Riposte altrui nella TL"
externalServices: "Servizi esterni"
impressum: "Dichiarazione di proprietà"
impressumUrl: "URL della dichiarazione di proprietà"
impressumDescription: "La dichiarazione di proprietà, è obbligatoria in alcuni paesi come la Germania (Impressum)."
privacyPolicy: "Informativa sulla privacy"
privacyPolicyUrl: "URL della informativa privacy"
tosAndPrivacyPolicy: "Condizioni d'uso e informativa sulla privacy"
_announcement: _announcement:
forExistingUsers: "Solo ai profili attuali" forExistingUsers: "Solo ai profili attuali"
forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio." forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio."
@ -1451,14 +1464,14 @@ _role:
_options: _options:
gtlAvailable: "Disponibilità della Timeline Federata" gtlAvailable: "Disponibilità della Timeline Federata"
ltlAvailable: "Disponibilità della Timeline Locale" ltlAvailable: "Disponibilità della Timeline Locale"
canPublicNote: "Può scrivere Note con Visibilità Pubblica" canPublicNote: "Scrivere Note con Visibilità Pubblica"
canInvite: "Genera codici di invito all'istanza" canInvite: "Generare codici di invito all'istanza"
inviteLimit: "Limite di codici invito" inviteLimit: "Limite di codici invito"
inviteLimitCycle: "Intervallo di emissione del codice di invito" inviteLimitCycle: "Intervallo di emissione del codice di invito"
inviteExpirationTime: "Scadenza del codice di invito" inviteExpirationTime: "Scadenza del codice di invito"
canManageCustomEmojis: "Gestire le emoji personalizzate" canManageCustomEmojis: "Gestire le emoji personalizzate"
driveCapacity: "Capienza del Drive" driveCapacity: "Capienza del Drive"
alwaysMarkNsfw: "Imposta sempre come NSFW" alwaysMarkNsfw: "Impostare sempre come esplicito (NSFW)"
pinMax: "Quantità massima di Note in primo piano" pinMax: "Quantità massima di Note in primo piano"
antennaMax: "Quantità massima di Antenne" antennaMax: "Quantità massima di Antenne"
wordMuteMax: "Lunghezza massima del filtro parole" wordMuteMax: "Lunghezza massima del filtro parole"
@ -1469,8 +1482,9 @@ _role:
userEachUserListsMax: "Quantità massima di profili per lista" userEachUserListsMax: "Quantità massima di profili per lista"
rateLimitFactor: "Limite del rapporto" rateLimitFactor: "Limite del rapporto"
descriptionOfRateLimitFactor: "I rapporti più bassi sono meno restrittivi, quelli più alti lo sono di più." descriptionOfRateLimitFactor: "I rapporti più bassi sono meno restrittivi, quelli più alti lo sono di più."
canHideAds: "Può nascondere i banner" canHideAds: "Nascondere i banner"
canSearchNotes: "Ricercare nelle Note" canSearchNotes: "Ricercare nelle Note"
canUseTranslator: "Tradurre le Note"
_condition: _condition:
isLocal: "Profilo locale" isLocal: "Profilo locale"
isRemote: "Profilo remoto" isRemote: "Profilo remoto"
@ -1519,6 +1533,10 @@ _ad:
reduceFrequencyOfThisAd: "Visualizza questa pubblicità meno spesso" reduceFrequencyOfThisAd: "Visualizza questa pubblicità meno spesso"
hide: "Nascondi" hide: "Nascondi"
timezoneinfo: "Il giorno della settimana è determinato in base al fuso orario del server." timezoneinfo: "Il giorno della settimana è determinato in base al fuso orario del server."
adsSettings: "Impostazioni banner"
notesPerOneAd: "Quantità di Note tra i banner"
setZeroToDisable: "Imposta 0 (zero) per disattivare la distribuzione dei banner durante gli aggiornamenti in tempo reale"
adsTooClose: "Attenzione, l'intervallo di pubblicazione dei banner è molto breve, potrebbe infastidire significativamente la fruizione"
_forgotPassword: _forgotPassword:
enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo profilo. Il collegamento necessario per ripristinare la password verrà inviato a questo indirizzo." enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo profilo. Il collegamento necessario per ripristinare la password verrà inviato a questo indirizzo."
ifNoEmail: "Se il tuo indirizzo email non risulta registrato, contatta l'amministrazione dell'istanza." ifNoEmail: "Se il tuo indirizzo email non risulta registrato, contatta l'amministrazione dell'istanza."
@ -1530,7 +1548,7 @@ _gallery:
unlike: "Non mi piace più" unlike: "Non mi piace più"
_email: _email:
_follow: _follow:
title: "Ha iniziato a seguirti" title: "Adesso ti segue"
_receiveFollowRequest: _receiveFollowRequest:
title: "Hai ricevuto una richiesta di follow" title: "Hai ricevuto una richiesta di follow"
_plugin: _plugin:
@ -1602,13 +1620,8 @@ _menuDisplay:
hide: "Nascondere" hide: "Nascondere"
_wordMute: _wordMute:
muteWords: "Parole da filtrare" muteWords: "Parole da filtrare"
muteWordsDescription: "Separare con uno spazio indica la condizione \"E\". Separare con una interruzione di riga, indica la condizione \"O\"" muteWordsDescription: "Sparando con uno spazio indichi la condizione E (and). Separando con un a capo, indichi la condizione O (or)."
muteWordsDescription2: "Se vuoi indicare delle Espressioni Regolari (regexp), metti la condizione all'interno di due slash (/)" muteWordsDescription2: "Se vuoi indicare delle Espressioni Regolari (regexp), metti la condizione all'interno di due slash (/)"
softDescription: "Verranno nascoste da tutte le Timeline quelle Note che soddisfano le seguenti condizioni"
hardDescription: "Impedisci alla istanza di caricare Note che soddisfano le seguenti condizioni. Le Note già filtrate sono già scomparse in modo irreversibile, fino al cambiamento delle condizioni. Dopo di che scompariranno quelle che soddisfano le nuove condizioni."
soft: "Leggero"
hard: "Pesante"
mutedNotes: "Note filtrate"
_instanceMute: _instanceMute:
instanceMuteDescription: "Disattiva tutte le note, le note di rinvio (condivisione) dell'istanza configurata, comprese le risposte agli utenti dell'istanza." instanceMuteDescription: "Disattiva tutte le note, le note di rinvio (condivisione) dell'istanza configurata, comprese le risposte agli utenti dell'istanza."
instanceMuteDescription2: "Impostazione separata da una nuova riga" instanceMuteDescription2: "Impostazione separata da una nuova riga"
@ -1617,7 +1630,7 @@ _instanceMute:
_theme: _theme:
explore: "Esplora temi" explore: "Esplora temi"
install: "Installa un tema" install: "Installa un tema"
manage: "Gerisci temi" manage: "Gestione temi"
code: "Codice tema" code: "Codice tema"
description: "Descrizione" description: "Descrizione"
installed: "{name} è installato" installed: "{name} è installato"
@ -1672,9 +1685,6 @@ _theme:
infoFg: "Testo di informazioni" infoFg: "Testo di informazioni"
infoWarnBg: "Sfondo degli avvisi" infoWarnBg: "Sfondo degli avvisi"
infoWarnFg: "Testo di avviso" infoWarnFg: "Testo di avviso"
cwBg: "Sfondo del CW"
cwFg: "Testo del pulsante CW"
cwHoverBg: "Sfondo del pulsante CW (sorvolato)"
toastBg: "Sfondo di notifica a comparsa" toastBg: "Sfondo di notifica a comparsa"
toastFg: "Testo di notifica a comparsa" toastFg: "Testo di notifica a comparsa"
buttonBg: "Sfondo del pulsante" buttonBg: "Sfondo del pulsante"
@ -1692,8 +1702,6 @@ _sfx:
note: "Nota" note: "Nota"
noteMy: "Mia nota" noteMy: "Mia nota"
notification: "Notifiche" notification: "Notifiche"
chat: "Messaggi"
chatBg: "Chat (sfondo)"
antenna: "Ricezione dell'antenna" antenna: "Ricezione dell'antenna"
channel: "Notifiche di canale" channel: "Notifiche di canale"
_ago: _ago:
@ -1878,7 +1886,7 @@ _visibility:
followersDescription: "Visibile solo ai tuoi follower" followersDescription: "Visibile solo ai tuoi follower"
specified: "Nota diretta" specified: "Nota diretta"
specifiedDescription: "Visibile solo ai profili menzionati" specifiedDescription: "Visibile solo ai profili menzionati"
disableFederation: "Non federare" disableFederation: "Senza federazione"
disableFederationDescription: "Non spedire attività alle altre istanze remote" disableFederationDescription: "Non spedire attività alle altre istanze remote"
_postForm: _postForm:
replyPlaceholder: "Rispondi a questa nota..." replyPlaceholder: "Rispondi a questa nota..."
@ -2018,7 +2026,7 @@ _notification:
youGotReply: "{name} ti ha risposto" youGotReply: "{name} ti ha risposto"
youGotQuote: "{name} ha citato la tua Nota e ha detto" youGotQuote: "{name} ha citato la tua Nota e ha detto"
youRenoted: "{name} ha rinotato" youRenoted: "{name} ha rinotato"
youWereFollowed: "Ha iniziato a seguirti" youWereFollowed: "Adesso ti segue"
youReceivedFollowRequest: "Hai ricevuto una richiesta di follow" youReceivedFollowRequest: "Hai ricevuto una richiesta di follow"
yourFollowRequestAccepted: "La tua richiesta di follow è stata accettata" yourFollowRequestAccepted: "La tua richiesta di follow è stata accettata"
pollEnded: "Risultati del sondaggio." pollEnded: "Risultati del sondaggio."
@ -2130,3 +2138,6 @@ _moderationLogTypes:
unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito" unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito"
resolveAbuseReport: "Segnalazione risolta" resolveAbuseReport: "Segnalazione risolta"
createInvitation: "Genera codice di invito" createInvitation: "Genera codice di invito"
createAd: "Banner creato"
deleteAd: "Banner eliminato"
updateAd: "Banner aggiornato"

View file

@ -1128,6 +1128,15 @@ edited: "編集済み"
notificationRecieveConfig: "通知の受信設定" notificationRecieveConfig: "通知の受信設定"
mutualFollow: "相互フォロー" mutualFollow: "相互フォロー"
fileAttachedOnly: "ファイル付きのみ" fileAttachedOnly: "ファイル付きのみ"
showRepliesToOthersInTimeline: "TLに他の人への返信を含める"
hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない"
externalServices: "外部サービス"
impressum: "運営者情報"
impressumUrl: "運営者情報URL"
impressumDescription: "ドイツなどの一部の国と地域では表示が義務付けられています(Impressum)。"
privacyPolicy: "プライバシーポリシー"
privacyPolicyUrl: "プライバシーポリシーURL"
tosAndPrivacyPolicy: "利用規約・プライバシーポリシー"
_announcement: _announcement:
forExistingUsers: "既存ユーザーのみ" forExistingUsers: "既存ユーザーのみ"
@ -1465,7 +1474,6 @@ _role:
gtlAvailable: "グローバルタイムラインの閲覧" gtlAvailable: "グローバルタイムラインの閲覧"
ltlAvailable: "ローカルタイムラインの閲覧" ltlAvailable: "ローカルタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可" canPublicNote: "パブリック投稿の許可"
canEditNote: "ノートの編集"
canInvite: "サーバー招待コードの発行" canInvite: "サーバー招待コードの発行"
inviteLimit: "招待コードの作成可能数" inviteLimit: "招待コードの作成可能数"
inviteLimitCycle: "招待コードの発行間隔" inviteLimitCycle: "招待コードの発行間隔"
@ -1540,6 +1548,10 @@ _ad:
reduceFrequencyOfThisAd: "この広告の表示頻度を下げる" reduceFrequencyOfThisAd: "この広告の表示頻度を下げる"
hide: "表示しない" hide: "表示しない"
timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されます。" timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されます。"
adsSettings: "広告配信設定"
notesPerOneAd: "リアルタイム更新中に広告を配信する間隔(ノートの個数)"
setZeroToDisable: "0でリアルタイム更新時の広告配信を無効"
adsTooClose: "広告の配信間隔が極めて短いため、ユーザー体験が著しく損われる可能性があります。"
_forgotPassword: _forgotPassword:
enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。" enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。"
@ -1638,11 +1650,6 @@ _wordMute:
muteWords: "ミュートするワード" muteWords: "ミュートするワード"
muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。" muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。"
muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。" muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。"
softDescription: "指定した条件のノートをタイムラインから隠します。"
hardDescription: "指定した条件のノートをタイムラインに追加しないようにします。追加されなかったノートは、条件を変更しても除外されたままになります。"
soft: "ソフト"
hard: "ハード"
mutedNotes: "ミュートされたノート"
_instanceMute: _instanceMute:
instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したサーバーの全てのートとRenoteをミュートします。" instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したサーバーの全てのートとRenoteをミュートします。"
@ -1709,9 +1716,6 @@ _theme:
infoFg: "情報の文字" infoFg: "情報の文字"
infoWarnBg: "警告の背景" infoWarnBg: "警告の背景"
infoWarnFg: "警告の文字" infoWarnFg: "警告の文字"
cwBg: "CW ボタンの背景"
cwFg: "CW ボタンの文字"
cwHoverBg: "CW ボタンの背景 (ホバー)"
toastBg: "通知トーストの背景" toastBg: "通知トーストの背景"
toastFg: "通知トーストの文字" toastFg: "通知トーストの文字"
buttonBg: "ボタンの背景" buttonBg: "ボタンの背景"
@ -1730,8 +1734,6 @@ _sfx:
note: "ノート" note: "ノート"
noteMy: "ノート(自分)" noteMy: "ノート(自分)"
notification: "通知" notification: "通知"
chat: "チャット"
chatBg: "チャット(バックグラウンド)"
antenna: "アンテナ受信" antenna: "アンテナ受信"
channel: "チャンネル通知" channel: "チャンネル通知"
@ -2203,3 +2205,12 @@ _moderationLogTypes:
createAd: "広告を作成" createAd: "広告を作成"
deleteAd: "広告を削除" deleteAd: "広告を削除"
updateAd: "広告を更新" updateAd: "広告を更新"
_fileViewer:
title: "ファイルの詳細"
type: "ファイルタイプ"
size: "ファイルサイズ"
url: "URL"
uploadedAt: "追加日"
attachedNotes: "添付されているノート"
thisPageCanBeSeenFromTheAuthor: "このページは、このファイルをアップロードしたユーザーしか閲覧できません。"

View file

@ -1588,11 +1588,6 @@ _wordMute:
muteWords: "ミュートするワード" muteWords: "ミュートするワード"
muteWordsDescription: "スペースで区切るとAND指定になって、改行で区切るとOR指定になるで。" muteWordsDescription: "スペースで区切るとAND指定になって、改行で区切るとOR指定になるで。"
muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になるで。" muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になるで。"
softDescription: "指定した条件のノートをタイムラインから隠すで。"
hardDescription: "指定した条件のノートをタイムラインに追加しないようにするで。追加せーへんかったかったノートは、条件を変えても除外されたままになるで。"
soft: "ソフト"
hard: "ハード"
mutedNotes: "ミュートされたノート"
_instanceMute: _instanceMute:
instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したインスタンスの全てのートとRenoteをミュートにするで。" instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したインスタンスの全てのートとRenoteをミュートにするで。"
instanceMuteDescription2: "改行で区切って設定するんやで" instanceMuteDescription2: "改行で区切って設定するんやで"
@ -1656,9 +1651,6 @@ _theme:
infoFg: "情報の文字" infoFg: "情報の文字"
infoWarnBg: "警告の背景" infoWarnBg: "警告の背景"
infoWarnFg: "警告の文字" infoWarnFg: "警告の文字"
cwBg: "CW ボタンの背景"
cwFg: "CW ボタンの文字"
cwHoverBg: "CW ボタンの背景 (ホバー)"
toastBg: "通知トーストの背景" toastBg: "通知トーストの背景"
toastFg: "通知トーストの文字" toastFg: "通知トーストの文字"
buttonBg: "ボタンの背景" buttonBg: "ボタンの背景"
@ -1676,8 +1668,6 @@ _sfx:
note: "ノート" note: "ノート"
noteMy: "ノート(自分)" noteMy: "ノート(自分)"
notification: "通知" notification: "通知"
chat: "チャット"
chatBg: "チャット(バックグラウンド)"
antenna: "アンテナ受信" antenna: "アンテナ受信"
channel: "チャンネル通知" channel: "チャンネル通知"
_ago: _ago:

View file

@ -1600,11 +1600,6 @@ _wordMute:
muteWords: "뮤트할 단어" muteWords: "뮤트할 단어"
muteWordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 OR로 지정됩니다." muteWordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 OR로 지정됩니다."
muteWordsDescription2: "정규 표현식을 사용하려면 키워드를 빗금표(/)로 감싸 주세요." muteWordsDescription2: "정규 표현식을 사용하려면 키워드를 빗금표(/)로 감싸 주세요."
softDescription: "지정한 조건의 노트를 타임라인에서 숨깁니다."
hardDescription: "지정한 조건의 노트를 타임라인에 추가하지 않습니다. 타임라인에 추가되지 않은 노트는 조건을 변경해도 표시되지 않습니다."
soft: "보통"
hard: "보다 높은 수준"
mutedNotes: "뮤트된 노트"
_instanceMute: _instanceMute:
instanceMuteDescription: "뮤트한 서버에서 오는 답글을 포함한 모든 노트와 Renote를 뮤트합니다." instanceMuteDescription: "뮤트한 서버에서 오는 답글을 포함한 모든 노트와 Renote를 뮤트합니다."
instanceMuteDescription2: "한 줄에 하나씩 입력해 주세요" instanceMuteDescription2: "한 줄에 하나씩 입력해 주세요"
@ -1668,9 +1663,6 @@ _theme:
infoFg: "정보창 텍스트" infoFg: "정보창 텍스트"
infoWarnBg: "경고창 배경" infoWarnBg: "경고창 배경"
infoWarnFg: "경고창 텍스트" infoWarnFg: "경고창 텍스트"
cwBg: "CW 버튼 배경"
cwFg: "CW 버튼 텍스트"
cwHoverBg: "CW 버튼 배경 (호버)"
toastBg: "알림창 배경" toastBg: "알림창 배경"
toastFg: "알림창 텍스트" toastFg: "알림창 텍스트"
buttonBg: "버튼 배경" buttonBg: "버튼 배경"
@ -1688,8 +1680,6 @@ _sfx:
note: "새 노트" note: "새 노트"
noteMy: "내 노트" noteMy: "내 노트"
notification: "알림" notification: "알림"
chat: "대화"
chatBg: "대화 (백그라운드)"
antenna: "안테나 수신" antenna: "안테나 수신"
channel: "채널 알림" channel: "채널 알림"
_ago: _ago:

View file

@ -407,7 +407,6 @@ _theme:
_sfx: _sfx:
note: "ບັນທຶກ" note: "ບັນທຶກ"
notification: "ການແຈ້ງເຕືອນ" notification: "ການແຈ້ງເຕືອນ"
chat: "ແຊ໋ດ"
_2fa: _2fa:
renewTOTPCancel: "ບໍ່​ແມ່ນ​ຕອນ​ນີ້" renewTOTPCancel: "ບໍ່​ແມ່ນ​ຕອນ​ນີ້"
_widgets: _widgets:

View file

@ -438,7 +438,6 @@ _theme:
_sfx: _sfx:
note: "Notities" note: "Notities"
notification: "Meldingen" notification: "Meldingen"
chat: "Chat"
_2fa: _2fa:
renewTOTPCancel: "Nee, bedankt" renewTOTPCancel: "Nee, bedankt"
_widgets: _widgets:

View file

@ -575,9 +575,6 @@ _channel:
nameAndDescription: "Navn og beskrivelse" nameAndDescription: "Navn og beskrivelse"
_menuDisplay: _menuDisplay:
hide: "Skjul" hide: "Skjul"
_wordMute:
soft: "Myk"
hard: "Hard"
_theme: _theme:
description: "Beskrivelse" description: "Beskrivelse"
color: "Farge" color: "Farge"

View file

@ -982,9 +982,6 @@ _menuDisplay:
_wordMute: _wordMute:
muteWords: "Słowo do wyciszenia" muteWords: "Słowo do wyciszenia"
muteWordsDescription2: "Otocz słowa kluczowe ukośnikami, aby używać wyrażeń regularnych." muteWordsDescription2: "Otocz słowa kluczowe ukośnikami, aby używać wyrażeń regularnych."
soft: "Łagodny"
hard: "Twardy"
mutedNotes: "Wyciszone wpisy"
_instanceMute: _instanceMute:
title: "Ukrywa wpisy z wymienionych instancji." title: "Ukrywa wpisy z wymienionych instancji."
heading: "Lista instancji do wyciszenia" heading: "Lista instancji do wyciszenia"
@ -1046,9 +1043,6 @@ _theme:
infoFg: "Tekst informacji" infoFg: "Tekst informacji"
infoWarnBg: "Tło ostrzeżenia" infoWarnBg: "Tło ostrzeżenia"
infoWarnFg: "Tekst ostrzeżenia" infoWarnFg: "Tekst ostrzeżenia"
cwBg: "Tło CW"
cwFg: "Tekst CW"
cwHoverBg: "Tło CW (po najechaniu)"
toastBg: "Tło powiadomień" toastBg: "Tło powiadomień"
toastFg: "Tekst powiadomień" toastFg: "Tekst powiadomień"
buttonBg: "Tło przycisku" buttonBg: "Tło przycisku"
@ -1066,8 +1060,6 @@ _sfx:
note: "Wpisy" note: "Wpisy"
noteMy: "Mój wpis" noteMy: "Mój wpis"
notification: "Powiadomienia" notification: "Powiadomienia"
chat: "Wiadomości"
chatBg: "Rozmowy (tło)"
antenna: "Anteny" antenna: "Anteny"
channel: "Powiadomienia kanału" channel: "Powiadomienia kanału"
_ago: _ago:

View file

@ -1320,7 +1320,6 @@ _theme:
_sfx: _sfx:
note: "Posts" note: "Posts"
notification: "Notificações" notification: "Notificações"
chat: "Chat"
_ago: _ago:
invalid: "Não há nada aqui" invalid: "Não há nada aqui"
_timelineTutorial: _timelineTutorial:

View file

@ -647,7 +647,6 @@ _theme:
_sfx: _sfx:
note: "Note" note: "Note"
notification: "Notificări" notification: "Notificări"
chat: "Chat"
_ago: _ago:
invalid: "Nu e nimic de văzut aici" invalid: "Nu e nimic de văzut aici"
_widgets: _widgets:

View file

@ -1488,11 +1488,6 @@ _wordMute:
muteWords: "Скрыть слово" muteWords: "Скрыть слово"
muteWordsDescription: "Пишите слова через пробел в одной строке, чтобы фильтровать их появление вместе; а если хотите фильтровать любое из них, пишите в отдельных строках." muteWordsDescription: "Пишите слова через пробел в одной строке, чтобы фильтровать их появление вместе; а если хотите фильтровать любое из них, пишите в отдельных строках."
muteWordsDescription2: "Здесь можно использовать регулярные выражения — просто заключите их между двумя дробными чертами (/)." muteWordsDescription2: "Здесь можно использовать регулярные выражения — просто заключите их между двумя дробными чертами (/)."
softDescription: "Соответствующие условиям заметки будут спрятаны из вашей ленты."
hardDescription: "Соответстующие условиям заметки вообще не будут попадать в вашу ленту. Даже если вы поменяете условия, отсеенные таким образом заметки уже не появятся."
soft: "Мягко"
hard: "Жёстко"
mutedNotes: "Скрытые заметки"
_instanceMute: _instanceMute:
instanceMuteDescription: "Заметки и репосты с указанных здесь инстансов, а также ответы пользователям оттуда же не будут отображаться." instanceMuteDescription: "Заметки и репосты с указанных здесь инстансов, а также ответы пользователям оттуда же не будут отображаться."
instanceMuteDescription2: "Пишите каждый инстанс на отдельной строке" instanceMuteDescription2: "Пишите каждый инстанс на отдельной строке"
@ -1556,9 +1551,6 @@ _theme:
infoFg: "Текст сообщения" infoFg: "Текст сообщения"
infoWarnBg: "Фон предупреждения" infoWarnBg: "Фон предупреждения"
infoWarnFg: "Текст предупреждения" infoWarnFg: "Текст предупреждения"
cwBg: "Фон предупреждения о содержимом"
cwFg: "Текст предупреждения о содержимом"
cwHoverBg: "Фон предупреждения о содержимом (под указателем)"
toastBg: "Фон оповещения" toastBg: "Фон оповещения"
toastFg: "Текст оповещения" toastFg: "Текст оповещения"
buttonBg: "Фон кнопки" buttonBg: "Фон кнопки"
@ -1576,8 +1568,6 @@ _sfx:
note: "Заметки" note: "Заметки"
noteMy: "Собственные заметки" noteMy: "Собственные заметки"
notification: "Уведомления" notification: "Уведомления"
chat: "Сообщения"
chatBg: "Сообщения (фон)"
antenna: "Антенна" antenna: "Антенна"
channel: "Канал" channel: "Канал"
_ago: _ago:

View file

@ -1039,11 +1039,6 @@ _wordMute:
muteWords: "Umlčané slová" muteWords: "Umlčané slová"
muteWordsDescription: "Medzerami oddeľte pre podmienku AND a novými riadkami pre podmienku OR." muteWordsDescription: "Medzerami oddeľte pre podmienku AND a novými riadkami pre podmienku OR."
muteWordsDescription2: "Regulárne výrazy sa použijú keď použijete okolo lomítka." muteWordsDescription2: "Regulárne výrazy sa použijú keď použijete okolo lomítka."
softDescription: "Skryje poznámky z časovej osi, ktoré spĺňajú podmienky."
hardDescription: "Zabráni poznámky spĺňajúce množinu podmienok, aby boli pridané do časovej osi. Navyše tieto poznámky nepribudnú v časovej osi ani keď sa podmienky zmenia."
soft: "Mäkké"
hard: "Tvrdé"
mutedNotes: "Umlčané poznámky"
_instanceMute: _instanceMute:
instanceMuteDescription: "Toto umlčí všetky poznámky/preposlania zo zoznamu serverov, vrátane tých, na ktoré používatelia odpovedajú z umlčaného servera." instanceMuteDescription: "Toto umlčí všetky poznámky/preposlania zo zoznamu serverov, vrátane tých, na ktoré používatelia odpovedajú z umlčaného servera."
instanceMuteDescription2: "Oddeľte novými riadkami" instanceMuteDescription2: "Oddeľte novými riadkami"
@ -1107,9 +1102,6 @@ _theme:
infoFg: "Informačný text" infoFg: "Informačný text"
infoWarnBg: "Pozadie varovania" infoWarnBg: "Pozadie varovania"
infoWarnFg: "Text varovania" infoWarnFg: "Text varovania"
cwBg: "CW pozadie tlačidla"
cwFg: "CW text tlačidla"
cwHoverBg: "CW pozadie tlačidla (pod kurzorom)"
toastBg: "Pozadie upozornenia" toastBg: "Pozadie upozornenia"
toastFg: "Text upozornenia" toastFg: "Text upozornenia"
buttonBg: "Pozadie tlačidla" buttonBg: "Pozadie tlačidla"
@ -1127,8 +1119,6 @@ _sfx:
note: "Poznámky" note: "Poznámky"
noteMy: "Vlastná poznámka" noteMy: "Vlastná poznámka"
notification: "Oznámenia" notification: "Oznámenia"
chat: "Chat"
chatBg: "Chat (pozadie)"
antenna: "Antény" antenna: "Antény"
channel: "Upozornenia kanála" channel: "Upozornenia kanála"
_ago: _ago:

View file

@ -507,7 +507,6 @@ _theme:
_sfx: _sfx:
note: "Noter" note: "Noter"
notification: "Notifikationer" notification: "Notifikationer"
chat: "Chatt"
antenna: "Antenner" antenna: "Antenner"
_2fa: _2fa:
renewTOTPCancel: "Nej tack" renewTOTPCancel: "Nej tack"

View file

@ -1150,6 +1150,7 @@ _serverRules:
description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ" description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ"
_serverSettings: _serverSettings:
iconUrl: "ไอคอน URL" iconUrl: "ไอคอน URL"
manifestJsonOverride: "manifest.json โอเวอร์ลาย"
shortName: "ชื่อย่อ" shortName: "ชื่อย่อ"
_accountMigration: _accountMigration:
moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง" moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง"
@ -1407,6 +1408,7 @@ _achievements:
flavor: "Misskey-Misskey La-Tu-Ma" flavor: "Misskey-Misskey La-Tu-Ma"
_smashTestNotificationButton: _smashTestNotificationButton:
title: "ทดสอบโอเวอร์โฟลว์" title: "ทดสอบโอเวอร์โฟลว์"
description: "ทดสอบการแจ้งเตือนทริกเกอร์ซ้ำๆ ภายในระยะเวลาอันสั้นๆ"
_role: _role:
new: "บทบาทใหม่" new: "บทบาทใหม่"
edit: "แก้ไขบทบาท" edit: "แก้ไขบทบาท"
@ -1445,7 +1447,6 @@ _role:
gtlAvailable: "การดูไทม์ไลน์ทั่วโลก" gtlAvailable: "การดูไทม์ไลน์ทั่วโลก"
ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น" ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น"
canPublicNote: "สามารถส่งโน้ตสาธารณะ" canPublicNote: "สามารถส่งโน้ตสาธารณะ"
canEditNote: "กำลังแก้ไขโน้ต"
canInvite: "สร้างรหัสเชิญอินสแตนซ์" canInvite: "สร้างรหัสเชิญอินสแตนซ์"
inviteLimit: "จำกัดการเชิญ" inviteLimit: "จำกัดการเชิญ"
inviteLimitCycle: "จำกัดการเชิญไว้คูลดาวน์" inviteLimitCycle: "จำกัดการเชิญไว้คูลดาวน์"
@ -1465,6 +1466,7 @@ _role:
descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า" descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า"
canHideAds: "ซ่อนโฆษณา" canHideAds: "ซ่อนโฆษณา"
canSearchNotes: "การใช้การค้นหาโน้ต" canSearchNotes: "การใช้การค้นหาโน้ต"
canUseTranslator: "การใช้งานแปล"
_condition: _condition:
isLocal: "ผู้ใช้ภายใน" isLocal: "ผู้ใช้ภายใน"
isRemote: "ผู้ใช้ระยะไกล" isRemote: "ผู้ใช้ระยะไกล"
@ -1598,11 +1600,6 @@ _wordMute:
muteWords: "ปิดเสียงคำ" muteWords: "ปิดเสียงคำ"
muteWordsDescription: "คั่นด้วยช่องว่างสำหรับเงื่อนไข AND หรือด้วยการขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR นะ" muteWordsDescription: "คั่นด้วยช่องว่างสำหรับเงื่อนไข AND หรือด้วยการขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR นะ"
muteWordsDescription2: "ล้อมรอบคีย์เวิร์ดด้วยเครื่องหมายทับเพื่อใช้นิพจน์ทั่วไป" muteWordsDescription2: "ล้อมรอบคีย์เวิร์ดด้วยเครื่องหมายทับเพื่อใช้นิพจน์ทั่วไป"
softDescription: "ซ่อนโน้ตให้ตรงตามเงื่อนไขที่ตั้งไว้จากไทม์ไลน์"
hardDescription: "ป้องกันไม่ให้โน้ตย่อที่ตรงตามเงื่อนไขที่ตั้งไว้ไม่ให้ถูกเพิ่มลงในไทม์ไลน์ นอกจากนี้ โน้ตเหล่านี้จะไม่ถูกเพิ่มลงในไทม์ไลน์แม้ว่าจะมีการเปลี่ยนแปลงเงื่อนไขยังไงก็ตาม"
soft: "ซอฟ"
hard: "ยาก"
mutedNotes: "ปิดเสียงโน้ต"
_instanceMute: _instanceMute:
instanceMuteDescription: "การดำเนินการนี้จะปิดเสียง\"โน้ต/รีโน้ต\"จากอินสแตนซ์ที่อยู่ในรายการ รวมถึงบันทึกของผู้ใช้ที่ตอบกลับผู้ใช้จากอินสแตนซ์ที่ปิดเสียง" instanceMuteDescription: "การดำเนินการนี้จะปิดเสียง\"โน้ต/รีโน้ต\"จากอินสแตนซ์ที่อยู่ในรายการ รวมถึงบันทึกของผู้ใช้ที่ตอบกลับผู้ใช้จากอินสแตนซ์ที่ปิดเสียง"
instanceMuteDescription2: "คั่นด้วยการขึ้นบรรทัดใหม่" instanceMuteDescription2: "คั่นด้วยการขึ้นบรรทัดใหม่"
@ -1666,9 +1663,6 @@ _theme:
infoFg: "ข้อความข้อมูล" infoFg: "ข้อความข้อมูล"
infoWarnBg: "คำเตือนพื้นหลัง" infoWarnBg: "คำเตือนพื้นหลัง"
infoWarnFg: "คำเตือนข้อความ" infoWarnFg: "คำเตือนข้อความ"
cwBg: "ปุ่ม CW พื้นหลัง"
cwFg: "ปุ่ม CW ข้อความ"
cwHoverBg: "ปุ่ม CW พื้นหลัง (โฮเวอร์)"
toastBg: "ประวัติการแจ้งเตือน" toastBg: "ประวัติการแจ้งเตือน"
toastFg: "ข้อความแจ้งเตือน" toastFg: "ข้อความแจ้งเตือน"
buttonBg: "ปุ่มพื้นหลัง" buttonBg: "ปุ่มพื้นหลัง"
@ -1686,8 +1680,6 @@ _sfx:
note: "หมายเหตุ" note: "หมายเหตุ"
noteMy: "โน้ตของตัวเอง" noteMy: "โน้ตของตัวเอง"
notification: "การเเจ้งเตือน" notification: "การเเจ้งเตือน"
chat: "แชท"
chatBg: "แชท (พื้นหลัง)"
antenna: "เสาอากาศ" antenna: "เสาอากาศ"
channel: "การแจ้งเตือนช่อง" channel: "การแจ้งเตือนช่อง"
_ago: _ago:
@ -1792,6 +1784,7 @@ _antennaSources:
homeTimeline: "โน้ตจากผู้ใช้ที่ติดตาม" homeTimeline: "โน้ตจากผู้ใช้ที่ติดตาม"
users: "โน้ตจากผู้ใช้ที่เฉพาะเจาะจง" users: "โน้ตจากผู้ใช้ที่เฉพาะเจาะจง"
userList: "โน้ตจากรายชื่อผู้ใช้ที่ระบุ" userList: "โน้ตจากรายชื่อผู้ใช้ที่ระบุ"
userBlacklist: "โน้ตทั้งหมดยกเว้นโน้ตของผู้ใช้ที่ต้องระบุเจาะจงตั้งแต่หนึ่งรายขึ้นไป"
_weekday: _weekday:
sunday: "วันอาทิตย์" sunday: "วันอาทิตย์"
monday: "วันจันทร์" monday: "วันจันทร์"
@ -1891,6 +1884,7 @@ _profile:
metadataContent: "เนื้อหา" metadataContent: "เนื้อหา"
changeAvatar: "เปลี่ยนอวาตาร์" changeAvatar: "เปลี่ยนอวาตาร์"
changeBanner: "เปลี่ยนแบนเนอร์" changeBanner: "เปลี่ยนแบนเนอร์"
verifiedLinkDescription: "โดยการป้อน URL ที่มีลิงก์ไปยังโปรไฟล์ของคุณตรงนี้ ส่วนไอคอนการยืนยันความเป็นเจ้าของนั้นก็สามารถแสดงถัดจากฟิลด์ได้นะ"
_exportOrImport: _exportOrImport:
allNotes: "โน้ตทั้งหมด" allNotes: "โน้ตทั้งหมด"
favoritedNotes: "บันทึกที่ชื่นชอบ" favoritedNotes: "บันทึกที่ชื่นชอบ"
@ -2104,7 +2098,17 @@ _moderationLogTypes:
updateUserNote: "อัปเดตโน้ตการกลั่นกรองแล้ว" updateUserNote: "อัปเดตโน้ตการกลั่นกรองแล้ว"
deleteDriveFile: "ลบไฟล์ออกแล้ว" deleteDriveFile: "ลบไฟล์ออกแล้ว"
deleteNote: "ลบโน้ตออกแล้ว" deleteNote: "ลบโน้ตออกแล้ว"
createGlobalAnnouncement: "สร้างประกาศทั่วโลกแล้ว"
createUserAnnouncement: "สร้างประกาศผู้ใช้แล้ว"
updateGlobalAnnouncement: "อัปเดตประกาศทั่วโลกแล้ว"
updateUserAnnouncement: "อัปเดตประกาศผู้ใช้แล้ว"
deleteGlobalAnnouncement: "ลบประกาศทั่วโลกออกแล้ว"
deleteUserAnnouncement: "ลบประกาศผู้ใช้ออกแล้ว"
resetPassword: "รีเซ็ตรหัสผ่าน" resetPassword: "รีเซ็ตรหัสผ่าน"
suspendRemoteInstance: "อินสแตนซ์ระยะไกลถูกระงับ"
unsuspendRemoteInstance: "อินสแตนซ์ระยะไกลเลิกการระงับ"
markSensitiveDriveFile: "ทำเครื่องหมายไฟล์บอกว่าละเอียดอ่อน"
unmarkSensitiveDriveFile: "ยกเลิกทำเครื่องหมายไฟล์ว่าละเอียดอ่อน"
resolveAbuseReport: "รายงานได้รับการแก้ไขแล้ว" resolveAbuseReport: "รายงานได้รับการแก้ไขแล้ว"
createInvitation: "สร้างคำเชิญ" createInvitation: "สร้างคำเชิญ"
createAd: "สร้างโฆษณาแล้ว" createAd: "สร้างโฆษณาแล้ว"

View file

@ -386,7 +386,6 @@ _theme:
_sfx: _sfx:
note: "notlar" note: "notlar"
notification: "Bildirim" notification: "Bildirim"
chat: "Mesajlar"
_2fa: _2fa:
renewTOTPCancel: "Hayır, teşekkürler" renewTOTPCancel: "Hayır, teşekkürler"
_permissions: _permissions:

View file

@ -1233,11 +1233,6 @@ _wordMute:
muteWords: "Заглушені слова" muteWords: "Заглушені слова"
muteWordsDescription: "Розділення ключових слів пробілами для \"І\" або з нової лінійки для \"АБО\"" muteWordsDescription: "Розділення ключових слів пробілами для \"І\" або з нової лінійки для \"АБО\""
muteWordsDescription2: "Для використання RegEx, ключові слова потрібно вписати поміж слешів \"/\"." muteWordsDescription2: "Для використання RegEx, ключові слова потрібно вписати поміж слешів \"/\"."
softDescription: "Приховати записи які відповідають критеріям зі стрічки подій."
hardDescription: "Приховати записи які відповідають критеріям зі стрічки подій. Також приховані записи не будуть додані до стрічки подій навіть якщо критерії буде змінено."
soft: "М'яко"
hard: "Жорстко"
mutedNotes: "Заблоковані нотатки"
_instanceMute: _instanceMute:
instanceMuteDescription2: "Розділяйте новими рядками" instanceMuteDescription2: "Розділяйте новими рядками"
title: "Приховує нотатки з перелічених інстансів." title: "Приховує нотатки з перелічених інстансів."
@ -1295,9 +1290,6 @@ _theme:
infoFg: "Текст інформації" infoFg: "Текст інформації"
infoWarnBg: "Фон попередження" infoWarnBg: "Фон попередження"
infoWarnFg: "Текст попередження" infoWarnFg: "Текст попередження"
cwBg: "Фон чутливого змісту"
cwFg: "Текст чутливого змісту"
cwHoverBg: "Фон чутливого змісту (при наведенні)"
toastBg: "Фон повідомлення" toastBg: "Фон повідомлення"
toastFg: "Текст повідомлення" toastFg: "Текст повідомлення"
buttonBg: "Фон кнопки" buttonBg: "Фон кнопки"
@ -1315,8 +1307,6 @@ _sfx:
note: "Нотатки" note: "Нотатки"
noteMy: "Мої нотатки" noteMy: "Мої нотатки"
notification: "Сповіщення" notification: "Сповіщення"
chat: "Чати"
chatBg: "Чати (фон)"
antenna: "Прийом антени" antenna: "Прийом антени"
channel: "Повідомлення каналу" channel: "Повідомлення каналу"
_ago: _ago:

View file

@ -910,7 +910,6 @@ _theme:
_sfx: _sfx:
note: "Qaydlar" note: "Qaydlar"
notification: "Xabarnomalar" notification: "Xabarnomalar"
chat: "Suhbat"
_ago: _ago:
minutesAgo: "{n} daqiqa oldin" minutesAgo: "{n} daqiqa oldin"
hoursAgo: "{n} soat oldin" hoursAgo: "{n} soat oldin"

View file

@ -1404,11 +1404,6 @@ _wordMute:
muteWords: "Ẩn từ ngữ" muteWords: "Ẩn từ ngữ"
muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition." muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition."
muteWordsDescription2: "Bao quanh các từ khóa bằng dấu gạch chéo để sử dụng cụm từ thông dụng." muteWordsDescription2: "Bao quanh các từ khóa bằng dấu gạch chéo để sử dụng cụm từ thông dụng."
softDescription: "Ẩn các tút phù hợp điều kiện đã đặt khỏi bảng tin."
hardDescription: "Ngăn các tút đáp ứng các điều kiện đã đặt xuất hiện trên bảng tin. Lưu ý, những tút này sẽ không được thêm vào bảng tin ngay cả khi các điều kiện được thay đổi."
soft: "Yếu"
hard: "Mạnh"
mutedNotes: "Những tút đã ẩn"
_instanceMute: _instanceMute:
instanceMuteDescription: "Thao tác này sẽ ẩn mọi tút/lượt đăng lại từ các máy chủ được liệt kê, bao gồm cả những tút dạng trả lời từ máy chủ bị ẩn." instanceMuteDescription: "Thao tác này sẽ ẩn mọi tút/lượt đăng lại từ các máy chủ được liệt kê, bao gồm cả những tút dạng trả lời từ máy chủ bị ẩn."
instanceMuteDescription2: "Tách bằng cách xuống dòng" instanceMuteDescription2: "Tách bằng cách xuống dòng"
@ -1472,9 +1467,6 @@ _theme:
infoFg: "Chữ thông tin" infoFg: "Chữ thông tin"
infoWarnBg: "Nền cảnh báo" infoWarnBg: "Nền cảnh báo"
infoWarnFg: "Chữ cảnh báo" infoWarnFg: "Chữ cảnh báo"
cwBg: "Nền nút nội dung ẩn"
cwFg: "Chữ nút nội dung ẩn"
cwHoverBg: "Nền nút nội dung ẩn (Chạm)"
toastBg: "Nền thông báo" toastBg: "Nền thông báo"
toastFg: "Chữ thông báo" toastFg: "Chữ thông báo"
buttonBg: "Nền nút" buttonBg: "Nền nút"
@ -1492,8 +1484,6 @@ _sfx:
note: "Tút" note: "Tút"
noteMy: "Tút của tôi" noteMy: "Tút của tôi"
notification: "Thông báo" notification: "Thông báo"
chat: "Trò chuyện"
chatBg: "Chat (Nền)"
antenna: "Trạm phát sóng" antenna: "Trạm phát sóng"
channel: "Kênh" channel: "Kênh"
_ago: _ago:

View file

@ -1126,6 +1126,8 @@ edited: "已编辑"
notificationRecieveConfig: "通知接收设置" notificationRecieveConfig: "通知接收设置"
mutualFollow: "互相关注" mutualFollow: "互相关注"
fileAttachedOnly: "仅限媒体" fileAttachedOnly: "仅限媒体"
showRepliesToOthersInTimeline: "在时间线上显示给其他人的回复"
hideRepliesToOthersInTimeline: "在时间线上隐藏给其他人的回复"
_announcement: _announcement:
forExistingUsers: "仅限现有用户" forExistingUsers: "仅限现有用户"
forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。" forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。"
@ -1456,7 +1458,6 @@ _role:
gtlAvailable: "查看全局时间线" gtlAvailable: "查看全局时间线"
ltlAvailable: "查看本地时间线" ltlAvailable: "查看本地时间线"
canPublicNote: "允许公开发帖" canPublicNote: "允许公开发帖"
canEditNote: "编辑帖子"
canInvite: "发放服务器邀请码" canInvite: "发放服务器邀请码"
inviteLimit: "可发行邀请码的数量" inviteLimit: "可发行邀请码的数量"
inviteLimitCycle: "邀请码的发行间隔" inviteLimitCycle: "邀请码的发行间隔"
@ -1609,11 +1610,6 @@ _wordMute:
muteWords: "禁用词" muteWords: "禁用词"
muteWordsDescription: "AND 条件用空格分隔OR 条件用换行符分隔。" muteWordsDescription: "AND 条件用空格分隔OR 条件用换行符分隔。"
muteWordsDescription2: "正则表达式用斜线包裹" muteWordsDescription2: "正则表达式用斜线包裹"
softDescription: "隐藏时间线中指定条件的帖子。"
hardDescription: "防止将具有指定条件的帖子添加到时间线。 即使您更改条件,未添加的帖文也会被排除在外。"
soft: "软屏蔽"
hard: "硬屏蔽"
mutedNotes: "被屏蔽的帖子"
_instanceMute: _instanceMute:
instanceMuteDescription: "屏蔽服务器中的所有帖子和转帖,包括这些服务器上的用户回复。" instanceMuteDescription: "屏蔽服务器中的所有帖子和转帖,包括这些服务器上的用户回复。"
instanceMuteDescription2: "一行一个" instanceMuteDescription2: "一行一个"
@ -1677,9 +1673,6 @@ _theme:
infoFg: "信息文本" infoFg: "信息文本"
infoWarnBg: "警告背景" infoWarnBg: "警告背景"
infoWarnFg: "警告文本" infoWarnFg: "警告文本"
cwBg: "隐藏内容按钮背景"
cwFg: "隐藏内容按钮文本"
cwHoverBg: "隐藏内容按钮背景(悬停)"
toastBg: "Toast 通知背景" toastBg: "Toast 通知背景"
toastFg: "Toast 通知文本" toastFg: "Toast 通知文本"
buttonBg: "按钮背景" buttonBg: "按钮背景"
@ -1697,8 +1690,6 @@ _sfx:
note: "帖子" note: "帖子"
noteMy: "我的帖子" noteMy: "我的帖子"
notification: "通知" notification: "通知"
chat: "聊天"
chatBg: "聊天背景"
antenna: "天线接收" antenna: "天线接收"
channel: "频道通知" channel: "频道通知"
_ago: _ago:

View file

@ -156,7 +156,7 @@ emojiUrl: "表情符號 URL"
addEmoji: "新增表情符號" addEmoji: "新增表情符號"
settingGuide: "推薦設定" settingGuide: "推薦設定"
cacheRemoteFiles: "快取遠端檔案" cacheRemoteFiles: "快取遠端檔案"
cacheRemoteFilesDescription: "啟用此設定後,遠端檔案會被快取在本伺服器的儲存空間中。雖然顯示圖片會變快,但會消耗較多伺服器的儲存空間。至於要快取遠端使用者到什麼程度,是依照角色的雲端硬碟容量而定。當超過這個限制時,從較舊的檔案開始自快取中刪除並改為連結。關閉這個設定時,遠端檔案從一開始就維持連結的方式,但建議將 default.yml 的 proxyRemoteFiles 設為 true以便產生圖片的縮圖並保護使用者的隱私。" cacheRemoteFilesDescription: "啟用此設定後,遠端檔案會被快取在本伺服器的儲存空間中。雖然顯示圖片會變快,但會消耗較多伺服器的儲存空間。至於要快取遠端使用者到什麼程度,是依照角色的雲端硬碟容量而定。當超過這個限制時,從較舊的檔案開始自快取中刪除並改為連結。關閉這個設定時,遠端檔案從一開始就維持連結的方式,但建議將 default.yml 的 proxyRemoteFiles 設為 true以便產生圖片的縮圖並保護使用者的隱私。"
youCanCleanRemoteFilesCache: "按檔案管理的🗑️按鈕,可將快取全部刪除。" youCanCleanRemoteFilesCache: "按檔案管理的🗑️按鈕,可將快取全部刪除。"
cacheRemoteSensitiveFiles: "快取遠端的敏感檔案" cacheRemoteSensitiveFiles: "快取遠端的敏感檔案"
cacheRemoteSensitiveFilesDescription: "若停用這個設定,則不會快取遠端的敏感檔案,而是直接連結。" cacheRemoteSensitiveFilesDescription: "若停用這個設定,則不會快取遠端的敏感檔案,而是直接連結。"
@ -1123,7 +1123,18 @@ authenticationRequiredToContinue: "請於繼續前完成驗證"
dateAndTime: "日期與時間" dateAndTime: "日期與時間"
showRenotes: "顯示轉發貼文" showRenotes: "顯示轉發貼文"
edited: "已編輯" edited: "已編輯"
notificationRecieveConfig: "接受通知的設定"
mutualFollow: "互相追隨" mutualFollow: "互相追隨"
fileAttachedOnly: "包含附件"
showRepliesToOthersInTimeline: "在時間軸上顯示給其他人的回覆"
hideRepliesToOthersInTimeline: "在時間軸上隱藏給其他人的回覆"
externalServices: "外部服務"
impressum: "營運者資訊"
impressumUrl: "營運者資訊網址"
impressumDescription: "在德國與部份地區必須要明確顯示營運者資訊。"
privacyPolicy: "隱私政策"
privacyPolicyUrl: "隱私政策網址"
tosAndPrivacyPolicy: "服務條款和隱私政策"
_announcement: _announcement:
forExistingUsers: "僅限既有的使用者" forExistingUsers: "僅限既有的使用者"
forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。" forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。"
@ -1454,7 +1465,6 @@ _role:
gtlAvailable: "瀏覽全域時間軸" gtlAvailable: "瀏覽全域時間軸"
ltlAvailable: "瀏覽本地時間軸" ltlAvailable: "瀏覽本地時間軸"
canPublicNote: "允許公開貼文" canPublicNote: "允許公開貼文"
canEditNote: "允許編輯貼文"
canInvite: "發行實例邀請碼" canInvite: "發行實例邀請碼"
inviteLimit: "可建立邀請碼的數量" inviteLimit: "可建立邀請碼的數量"
inviteLimitCycle: "邀請碼的發放間隔" inviteLimitCycle: "邀請碼的發放間隔"
@ -1474,6 +1484,7 @@ _role:
descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。" descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。"
canHideAds: "不顯示廣告" canHideAds: "不顯示廣告"
canSearchNotes: "可否搜尋貼文" canSearchNotes: "可否搜尋貼文"
canUseTranslator: "使用翻譯功能"
_condition: _condition:
isLocal: "本地使用者" isLocal: "本地使用者"
isRemote: "遠端使用者" isRemote: "遠端使用者"
@ -1522,6 +1533,10 @@ _ad:
reduceFrequencyOfThisAd: "降低此廣告的頻率 " reduceFrequencyOfThisAd: "降低此廣告的頻率 "
hide: "隱藏" hide: "隱藏"
timezoneinfo: "星期幾是由伺服器的時區指定的。" timezoneinfo: "星期幾是由伺服器的時區指定的。"
adsSettings: "廣告投放設定"
notesPerOneAd: "即時更新中投放廣告的間隔(貼文數)"
setZeroToDisable: "設為 0 則在即時更新時不投放廣告"
adsTooClose: "由於廣告投放的間隔極短,可能會嚴重影響使用者體驗。"
_forgotPassword: _forgotPassword:
enterEmail: "請輸入您的帳戶註冊的電子郵件地址。 密碼重置連結將被發送到該電子郵件地址。" enterEmail: "請輸入您的帳戶註冊的電子郵件地址。 密碼重置連結將被發送到該電子郵件地址。"
ifNoEmail: "如果您還沒有註冊您的電子郵件地址,請聯繫管理員。 " ifNoEmail: "如果您還沒有註冊您的電子郵件地址,請聯繫管理員。 "
@ -1607,11 +1622,6 @@ _wordMute:
muteWords: "加入靜音文字" muteWords: "加入靜音文字"
muteWordsDescription: "空格代表「以及」AND換行代表「或者」OR。" muteWordsDescription: "空格代表「以及」AND換行代表「或者」OR。"
muteWordsDescription2: "用斜線包圍關鍵字代表正規表達式。" muteWordsDescription2: "用斜線包圍關鍵字代表正規表達式。"
softDescription: "隱藏時間軸中符合特定條件的貼文。"
hardDescription: "符合特定條件的貼文將不會新增至時間軸。 即使您更改條件,未被新增的貼文也會被排除在外。"
soft: "軟性靜音"
hard: "硬性靜音"
mutedNotes: "已靜音的貼文"
_instanceMute: _instanceMute:
instanceMuteDescription: "包括對被靜音實例上的使用者的回覆,被設定的實例上所有貼文及轉發都會被靜音。" instanceMuteDescription: "包括對被靜音實例上的使用者的回覆,被設定的實例上所有貼文及轉發都會被靜音。"
instanceMuteDescription2: "設定時以換行進行分隔" instanceMuteDescription2: "設定時以換行進行分隔"
@ -1675,9 +1685,6 @@ _theme:
infoFg: "資訊內容" infoFg: "資訊內容"
infoWarnBg: "警告背景" infoWarnBg: "警告背景"
infoWarnFg: "警告文字" infoWarnFg: "警告文字"
cwBg: "隱藏內容按鈕背景"
cwFg: "隱藏內容按鈕文字"
cwHoverBg: "隱藏內容按鈕背景(懸浮)"
toastBg: "通知背景" toastBg: "通知背景"
toastFg: "通知文本" toastFg: "通知文本"
buttonBg: "按鈕背景" buttonBg: "按鈕背景"
@ -1695,8 +1702,6 @@ _sfx:
note: "貼文" note: "貼文"
noteMy: "我的貼文" noteMy: "我的貼文"
notification: "通知" notification: "通知"
chat: "聊天"
chatBg: "聊天背景"
antenna: "天線接收" antenna: "天線接收"
channel: "頻道通知" channel: "頻道通知"
_ago: _ago:
@ -2136,3 +2141,11 @@ _moderationLogTypes:
createAd: "建立廣告" createAd: "建立廣告"
deleteAd: "刪除廣告" deleteAd: "刪除廣告"
updateAd: "更新廣告" updateAd: "更新廣告"
_fileViewer:
title: "檔案詳細資訊"
type: "檔案類型 "
size: "檔案大小"
url: "URL"
uploadedAt: "加入日期"
attachedNotes: "含有附件的貼文"
thisPageCanBeSeenFromTheAuthor: "本頁面僅限上傳了這個檔案的使用者可以檢視。"

View file

@ -1,6 +1,6 @@
{ {
"name": "sharkey", "name": "sharkey",
"version": "2023.9.1.beta5", "version": "2023.10.1.beta1",
"codename": "shonk", "codename": "shonk",
"repository": { "repository": {
"type": "git", "type": "git",
@ -47,15 +47,15 @@
"cssnano": "6.0.1", "cssnano": "6.0.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"postcss": "8.4.31", "postcss": "8.4.31",
"terser": "5.20.0", "terser": "5.21.0",
"typescript": "5.2.2" "typescript": "5.2.2"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "6.7.3", "@typescript-eslint/eslint-plugin": "6.7.5",
"@typescript-eslint/parser": "6.7.3", "@typescript-eslint/parser": "6.7.5",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "13.3.0", "cypress": "13.3.0",
"eslint": "8.50.0", "eslint": "8.51.0",
"start-server-and-test": "2.0.1" "start-server-and-test": "2.0.1"
}, },
"optionalDependencies": { "optionalDependencies": {

View file

@ -216,4 +216,6 @@ module.exports = {
maxWorkers: 1, // Make it use worker (that can be killed and restarted) maxWorkers: 1, // Make it use worker (that can be killed and restarted)
logHeapUsage: true, // To debug when out-of-memory happens on CI logHeapUsage: true, // To debug when out-of-memory happens on CI
workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB) workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB)
maxConcurrency: 32,
}; };

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class AddSomeUrls1696003580220 {
name = 'AddSomeUrls1696003580220'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "impressumUrl" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "privacyPolicyUrl" character varying(1024)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "impressumUrl"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "privacyPolicyUrl"`);
}
}

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class WithReplies1696222183852 {
name = 'WithReplies1696222183852'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "following" ADD "withReplies" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "user_list_joining" ADD "withReplies" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`CREATE INDEX "IDX_d74d8ab5efa7e3bb82825c0fa2" ON "following" ("followeeId", "followerHost") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_d74d8ab5efa7e3bb82825c0fa2"`);
await queryRunner.query(`ALTER TABLE "user_list_joining" DROP COLUMN "withReplies"`);
await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "withReplies"`);
}
}

View file

@ -0,0 +1,11 @@
export class UserListMembership1696323464251 {
name = 'UserListMembership1696323464251'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_list_joining" RENAME TO "user_list_membership"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_list_membership" RENAME TO "user_list_joining"`);
}
}

View file

@ -0,0 +1,17 @@
export class Hibernation1696331570827 {
name = 'Hibernation1696331570827'
async up(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_d74d8ab5efa7e3bb82825c0fa2"`);
await queryRunner.query(`ALTER TABLE "user" ADD "isHibernated" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "following" ADD "isFollowerHibernated" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`CREATE INDEX "IDX_ce62b50d882d4e9dee10ad0d2f" ON "following" ("followeeId", "followerHost", "isFollowerHibernated") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_ce62b50d882d4e9dee10ad0d2f"`);
await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "isFollowerHibernated"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isHibernated"`);
await queryRunner.query(`CREATE INDEX "IDX_d74d8ab5efa7e3bb82825c0fa2" ON "following" ("followeeId", "followerHost") `);
}
}

View file

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class MetaCacheSettings1696373953614 {
name = 'MetaCacheSettings1696373953614'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "perLocalUserUserTimelineCacheMax" integer NOT NULL DEFAULT '800'`);
await queryRunner.query(`ALTER TABLE "meta" ADD "perRemoteUserUserTimelineCacheMax" integer NOT NULL DEFAULT '800'`);
await queryRunner.query(`ALTER TABLE "meta" ADD "perUserHomeTimelineCacheMax" integer NOT NULL DEFAULT '800'`);
await queryRunner.query(`ALTER TABLE "meta" ADD "perUserListTimelineCacheMax" integer NOT NULL DEFAULT '800'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perUserListTimelineCacheMax"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perUserHomeTimelineCacheMax"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perRemoteUserUserTimelineCacheMax"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perLocalUserUserTimelineCacheMax"`);
}
}

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class CleanUp1696405744672 {
name = 'CleanUp1696405744672'
async up(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_e7c0567f5261063592f022e9b5"`);
await queryRunner.query(`DROP INDEX "public"."IDX_25dfc71b0369b003a4cd434d0b"`);
}
async down(queryRunner) {
await queryRunner.query(`CREATE INDEX "IDX_25dfc71b0369b003a4cd434d0b" ON "note" ("attachedFileTypes") `);
await queryRunner.query(`CREATE INDEX "IDX_e7c0567f5261063592f022e9b5" ON "note" ("createdAt") `);
}
}

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class CleanUp1696569742153 {
name = 'CleanUp1696569742153'
async up(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_01f4581f114e0ebd2bbb876f0b"`);
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "score"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" ADD "score" integer NOT NULL DEFAULT '0'`);
await queryRunner.query(`CREATE INDEX "IDX_01f4581f114e0ebd2bbb876f0b" ON "note_reaction" ("createdAt") `);
}
}

View file

@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class CleanUp1696581429196 {
name = 'CleanUp1696581429196'
async up(queryRunner) {
await queryRunner.query(`DROP TABLE IF EXISTS "muted_note"`);
}
async down(queryRunner) {
}
}

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class AdsOnStream1696743032098 {
name = 'AdsOnStream1696743032098'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "notesPerOneAd" integer NOT NULL DEFAULT '0'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "notesPerOneAd"`);
}
}

View file

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class UserListUserId1696807733453 {
name = 'UserListUserId1696807733453'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_list_membership" ADD "userListUserId" character varying(32) NOT NULL DEFAULT ''`);
const memberships = await queryRunner.query(`SELECT "id", "userListId" FROM "user_list_membership"`);
for(let i = 0; i < memberships.length; i++) {
const userList = await queryRunner.query(`SELECT "userId" FROM "user_list" WHERE "id" = $1`, [memberships[i].userListId]);
await queryRunner.query(`UPDATE "user_list_membership" SET "userListUserId" = $1 WHERE "id" = $2`, [userList[0].userId, memberships[i].id]);
}
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_list_membership" DROP COLUMN "userListUserId"`);
}
}

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class UserListUserId21696808725134 {
name = 'UserListUserId21696808725134'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_list_membership" ALTER COLUMN "userListUserId" DROP DEFAULT`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_list_membership" ALTER COLUMN "userListUserId" SET DEFAULT ''`);
}
}

View file

@ -70,15 +70,15 @@
"@fastify/multipart": "8.0.0", "@fastify/multipart": "8.0.0",
"@fastify/static": "6.11.2", "@fastify/static": "6.11.2",
"@fastify/view": "8.2.0", "@fastify/view": "8.2.0",
"@nestjs/common": "10.2.6", "@nestjs/common": "10.2.7",
"@nestjs/core": "10.2.6", "@nestjs/core": "10.2.7",
"@nestjs/testing": "10.2.6", "@nestjs/testing": "10.2.7",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@simplewebauthn/server": "8.2.0", "@simplewebauthn/server": "8.2.0",
"@sinonjs/fake-timers": "11.1.0", "@sinonjs/fake-timers": "11.1.0",
"@smithy/node-http-handler": "2.1.5", "@smithy/node-http-handler": "2.1.5",
"@swc/cli": "0.1.62", "@swc/cli": "0.1.62",
"@swc/core": "1.3.90", "@swc/core": "1.3.92",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.12.0", "ajv": "8.12.0",
"archiver": "6.0.1", "archiver": "6.0.1",
@ -87,7 +87,7 @@
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "1.20.2", "body-parser": "1.20.2",
"bullmq": "4.11.4", "bullmq": "4.12.3",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.1", "cbor": "9.0.1",
"chalk": "5.3.0", "chalk": "5.3.0",
@ -127,13 +127,13 @@
"nanoid": "5.0.1", "nanoid": "5.0.1",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"nodemailer": "6.9.5", "nodemailer": "6.9.6",
"nsfwjs": "2.4.2", "nsfwjs": "2.4.2",
"oauth": "0.10.0", "oauth": "0.10.0",
"oauth2orize": "1.11.1", "oauth2orize": "1.11.1",
"oauth2orize-pkce": "0.1.2", "oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"otpauth": "9.1.4", "otpauth": "9.1.5",
"parse5": "7.1.2", "parse5": "7.1.2",
"pg": "8.11.3", "pg": "8.11.3",
"pkce-challenge": "4.0.1", "pkce-challenge": "4.0.1",
@ -158,7 +158,7 @@
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"summaly": "github:misskey-dev/summaly", "summaly": "github:misskey-dev/summaly",
"systeminformation": "5.21.9", "systeminformation": "5.21.11",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.1", "tmp": "0.2.1",
"tsc-alias": "1.8.8", "tsc-alias": "1.8.8",
@ -193,13 +193,13 @@
"@types/jsrsasign": "10.5.9", "@types/jsrsasign": "10.5.9",
"@types/mime-types": "2.1.2", "@types/mime-types": "2.1.2",
"@types/ms": "0.7.32", "@types/ms": "0.7.32",
"@types/node": "20.7.1", "@types/node": "20.8.4",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.11", "@types/nodemailer": "6.4.11",
"@types/oauth": "0.9.2", "@types/oauth": "0.9.2",
"@types/oauth2orize": "1.11.1", "@types/oauth2orize": "1.11.1",
"@types/oauth2orize-pkce": "0.1.0", "@types/oauth2orize-pkce": "0.1.0",
"@types/pg": "8.10.3", "@types/pg": "8.10.4",
"@types/pug": "2.0.7", "@types/pug": "2.0.7",
"@types/punycode": "2.1.0", "@types/punycode": "2.1.0",
"@types/qrcode": "1.5.2", "@types/qrcode": "1.5.2",
@ -217,11 +217,11 @@
"@types/vary": "1.1.1", "@types/vary": "1.1.1",
"@types/web-push": "3.6.1", "@types/web-push": "3.6.1",
"@types/ws": "8.5.6", "@types/ws": "8.5.6",
"@typescript-eslint/eslint-plugin": "6.7.3", "@typescript-eslint/eslint-plugin": "6.7.5",
"@typescript-eslint/parser": "6.7.3", "@typescript-eslint/parser": "6.7.5",
"aws-sdk-client-mock": "3.0.0", "aws-sdk-client-mock": "3.0.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.50.0", "eslint": "8.51.0",
"eslint-plugin-import": "2.28.1", "eslint-plugin-import": "2.28.1",
"execa": "8.0.1", "execa": "8.0.1",
"jest": "29.7.0", "jest": "29.7.0",

View file

@ -70,11 +70,19 @@ const $redisForSub: Provider = {
inject: [DI.config], inject: [DI.config],
}; };
const $redisForTimelines: Provider = {
provide: DI.redisForTimelines,
useFactory: (config: Config) => {
return new Redis.Redis(config.redisForTimelines);
},
inject: [DI.config],
};
@Global() @Global()
@Module({ @Module({
imports: [RepositoryModule], imports: [RepositoryModule],
providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub], providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines],
exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, RepositoryModule], exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, RepositoryModule],
}) })
export class GlobalModule implements OnApplicationShutdown { export class GlobalModule implements OnApplicationShutdown {
constructor( constructor(
@ -82,6 +90,7 @@ export class GlobalModule implements OnApplicationShutdown {
@Inject(DI.redis) private redisClient: Redis.Redis, @Inject(DI.redis) private redisClient: Redis.Redis,
@Inject(DI.redisForPub) private redisForPub: Redis.Redis, @Inject(DI.redisForPub) private redisForPub: Redis.Redis,
@Inject(DI.redisForSub) private redisForSub: Redis.Redis, @Inject(DI.redisForSub) private redisForSub: Redis.Redis,
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
) {} ) {}
public async dispose(): Promise<void> { public async dispose(): Promise<void> {
@ -98,6 +107,7 @@ export class GlobalModule implements OnApplicationShutdown {
this.redisClient.disconnect(), this.redisClient.disconnect(),
this.redisForPub.disconnect(), this.redisForPub.disconnect(),
this.redisForSub.disconnect(), this.redisForSub.disconnect(),
this.redisForTimelines.disconnect(),
]); ]);
} }

View file

@ -17,7 +17,6 @@ export async function server() {
const app = await NestFactory.createApplicationContext(MainModule, { const app = await NestFactory.createApplicationContext(MainModule, {
logger: new NestLogger(), logger: new NestLogger(),
}); });
app.enableShutdownHooks();
const serverService = app.get(ServerService); const serverService = app.get(ServerService);
await serverService.launch(); await serverService.launch();
@ -35,7 +34,6 @@ export async function jobQueue() {
const jobQueue = await NestFactory.createApplicationContext(QueueProcessorModule, { const jobQueue = await NestFactory.createApplicationContext(QueueProcessorModule, {
logger: new NestLogger(), logger: new NestLogger(),
}); });
jobQueue.enableShutdownHooks();
jobQueue.get(QueueProcessorService).start(); jobQueue.get(QueueProcessorService).start();
jobQueue.get(ChartManagementService).start(); jobQueue.get(ChartManagementService).start();

View file

@ -47,6 +47,7 @@ type Source = {
redis: RedisOptionsSource; redis: RedisOptionsSource;
redisForPubsub?: RedisOptionsSource; redisForPubsub?: RedisOptionsSource;
redisForJobQueue?: RedisOptionsSource; redisForJobQueue?: RedisOptionsSource;
redisForTimelines?: RedisOptionsSource;
meilisearch?: { meilisearch?: {
host: string; host: string;
port: string; port: string;
@ -161,6 +162,7 @@ export type Config = {
redis: RedisOptions & RedisOptionsSource; redis: RedisOptions & RedisOptionsSource;
redisForPubsub: RedisOptions & RedisOptionsSource; redisForPubsub: RedisOptions & RedisOptionsSource;
redisForJobQueue: RedisOptions & RedisOptionsSource; redisForJobQueue: RedisOptions & RedisOptionsSource;
redisForTimelines: RedisOptions & RedisOptionsSource;
perChannelMaxNoteCacheCount: number; perChannelMaxNoteCacheCount: number;
perUserNotificationsMaxCount: number; perUserNotificationsMaxCount: number;
deactivateAntennaThreshold: number; deactivateAntennaThreshold: number;
@ -227,6 +229,7 @@ export function loadConfig(): Config {
redis, redis,
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis, redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis, redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
id: config.id, id: config.id,
proxy: config.proxy, proxy: config.proxy,
proxySmtp: config.proxySmtp, proxySmtp: config.proxySmtp,

View file

@ -9,7 +9,7 @@ import { IsNull, In, MoreThan, Not } from 'typeorm';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/_.js'; import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
import type { RelationshipJobData, ThinUser } from '@/queue/types.js'; import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
@ -42,8 +42,8 @@ export class AccountMoveService {
@Inject(DI.mutingsRepository) @Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository, private mutingsRepository: MutingsRepository,
@Inject(DI.userListJoiningsRepository) @Inject(DI.userListMembershipsRepository)
private userListJoiningsRepository: UserListJoiningsRepository, private userListMembershipsRepository: UserListMembershipsRepository,
@Inject(DI.instancesRepository) @Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository, private instancesRepository: InstancesRepository,
@ -215,40 +215,41 @@ export class AccountMoveService {
@bindThis @bindThis
public async updateLists(src: ThinUser, dst: MiUser): Promise<void> { public async updateLists(src: ThinUser, dst: MiUser): Promise<void> {
// Return if there is no list to be updated. // Return if there is no list to be updated.
const oldJoinings = await this.userListJoiningsRepository.find({ const oldMemberships = await this.userListMembershipsRepository.find({
where: { where: {
userId: src.id, userId: src.id,
}, },
}); });
if (oldJoinings.length === 0) return; if (oldMemberships.length === 0) return;
const existingUserListIds = await this.userListJoiningsRepository.find({ const existingUserListIds = await this.userListMembershipsRepository.find({
where: { where: {
userId: dst.id, userId: dst.id,
}, },
}).then(joinings => joinings.map(joining => joining.userListId)); }).then(memberships => memberships.map(membership => membership.userListId));
const newJoinings: Map<string, { createdAt: Date; userId: string; userListId: string; }> = new Map(); const newMemberships: Map<string, { createdAt: Date; userId: string; userListId: string; userListUserId: string; }> = new Map();
// 重複しないようにIDを生成 // 重複しないようにIDを生成
const genId = (): string => { const genId = (): string => {
let id: string; let id: string;
do { do {
id = this.idService.genId(); id = this.idService.genId();
} while (newJoinings.has(id)); } while (newMemberships.has(id));
return id; return id;
}; };
for (const joining of oldJoinings) { for (const membership of oldMemberships) {
if (existingUserListIds.includes(joining.userListId)) continue; // skip if dst exists in this user's list if (existingUserListIds.includes(membership.userListId)) continue; // skip if dst exists in this user's list
newJoinings.set(genId(), { newMemberships.set(genId(), {
createdAt: new Date(), createdAt: new Date(),
userId: dst.id, userId: dst.id,
userListId: joining.userListId, userListId: membership.userListId,
userListUserId: membership.userListUserId,
}); });
} }
const arrayToInsert = Array.from(newJoinings.entries()).map(entry => ({ ...entry[1], id: entry[0] })); const arrayToInsert = Array.from(newMemberships.entries()).map(entry => ({ ...entry[1], id: entry[0] }));
await this.userListJoiningsRepository.insert(arrayToInsert); await this.userListMembershipsRepository.insert(arrayToInsert);
// Have the proxy account follow the new account in the same way as UserListService.push // Have the proxy account follow the new account in the same way as UserListService.push
if (this.userEntityService.isRemoteUser(dst)) { if (this.userEntityService.isRemoteUser(dst)) {

View file

@ -158,9 +158,13 @@ export class AnnouncementService {
if (moderator) { if (moderator) {
if (announcement.userId) { if (announcement.userId) {
const user = await this.usersRepository.findOneByOrFail({ id: announcement.userId });
this.moderationLogService.log(moderator, 'deleteUserAnnouncement', { this.moderationLogService.log(moderator, 'deleteUserAnnouncement', {
announcementId: announcement.id, announcementId: announcement.id,
announcement: announcement, announcement: announcement,
userId: announcement.userId,
userUsername: user.username,
userHost: user.host,
}); });
} else { } else {
this.moderationLogService.log(moderator, 'deleteGlobalAnnouncement', { this.moderationLogService.log(moderator, 'deleteGlobalAnnouncement', {

View file

@ -12,10 +12,11 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import * as Acct from '@/misc/acct.js'; import * as Acct from '@/misc/acct.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { AntennasRepository, UserListJoiningsRepository } from '@/models/_.js'; import type { AntennasRepository, UserListMembershipsRepository } from '@/models/_.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { RedisTimelineService } from '@/core/RedisTimelineService.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable() @Injectable()
@ -24,8 +25,8 @@ export class AntennaService implements OnApplicationShutdown {
private antennas: MiAntenna[]; private antennas: MiAntenna[];
constructor( constructor(
@Inject(DI.redis) @Inject(DI.redisForTimelines)
private redisClient: Redis.Redis, private redisForTimelines: Redis.Redis,
@Inject(DI.redisForSub) @Inject(DI.redisForSub)
private redisForSub: Redis.Redis, private redisForSub: Redis.Redis,
@ -33,11 +34,12 @@ export class AntennaService implements OnApplicationShutdown {
@Inject(DI.antennasRepository) @Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository, private antennasRepository: AntennasRepository,
@Inject(DI.userListJoiningsRepository) @Inject(DI.userListMembershipsRepository)
private userListJoiningsRepository: UserListJoiningsRepository, private userListMembershipsRepository: UserListMembershipsRepository,
private utilityService: UtilityService, private utilityService: UtilityService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private redisTimelineService: RedisTimelineService,
) { ) {
this.antennasFetched = false; this.antennasFetched = false;
this.antennas = []; this.antennas = [];
@ -81,15 +83,10 @@ export class AntennaService implements OnApplicationShutdown {
const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const))); const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const)));
const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna); const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna);
const redisPipeline = this.redisClient.pipeline(); const redisPipeline = this.redisForTimelines.pipeline();
for (const antenna of matchedAntennas) { for (const antenna of matchedAntennas) {
redisPipeline.xadd( this.redisTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline);
`antennaTimeline:${antenna.id}`,
'MAXLEN', '~', '200',
'*',
'note', note.id);
this.globalEventService.publishAntennaStream(antenna.id, 'note', note); this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
} }
@ -108,7 +105,7 @@ export class AntennaService implements OnApplicationShutdown {
if (antenna.src === 'home') { if (antenna.src === 'home') {
// TODO // TODO
} else if (antenna.src === 'list') { } else if (antenna.src === 'list') {
const listUsers = (await this.userListJoiningsRepository.findBy({ const listUsers = (await this.userListMembershipsRepository.findBy({
userListId: antenna.userListId!, userListId: antenna.userListId!,
})).map(x => x.userId); })).map(x => x.userId);

View file

@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js';
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js'; import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
import type { MiLocalUser, MiUser } from '@/models/User.js'; import type { MiLocalUser, MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -25,7 +25,7 @@ export class CacheService implements OnApplicationShutdown {
public userBlockingCache: RedisKVCache<Set<string>>; public userBlockingCache: RedisKVCache<Set<string>>;
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
public renoteMutingsCache: RedisKVCache<Set<string>>; public renoteMutingsCache: RedisKVCache<Set<string>>;
public userFollowingsCache: RedisKVCache<Set<string>>; public userFollowingsCache: RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>;
public userFollowingChannelsCache: RedisKVCache<Set<string>>; public userFollowingChannelsCache: RedisKVCache<Set<string>>;
constructor( constructor(
@ -136,12 +136,18 @@ export class CacheService implements OnApplicationShutdown {
fromRedisConverter: (value) => new Set(JSON.parse(value)), fromRedisConverter: (value) => new Set(JSON.parse(value)),
}); });
this.userFollowingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowings', { this.userFollowingsCache = new RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>(this.redisClient, 'userFollowings', {
lifetime: 1000 * 60 * 30, // 30m lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))), fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId', 'withReplies'] }).then(xs => {
toRedisConverter: (value) => JSON.stringify(Array.from(value)), const obj: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
fromRedisConverter: (value) => new Set(JSON.parse(value)), for (const x of xs) {
obj[x.followeeId] = { withReplies: x.withReplies };
}
return obj;
}),
toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => JSON.parse(value),
}); });
this.userFollowingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowingChannels', { this.userFollowingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowingChannels', {
@ -188,6 +194,7 @@ export class CacheService implements OnApplicationShutdown {
if (follower) follower.followingCount++; if (follower) follower.followingCount++;
const followee = this.userByIdCache.get(body.followeeId); const followee = this.userByIdCache.get(body.followeeId);
if (followee) followee.followersCount++; if (followee) followee.followersCount++;
this.userFollowingsCache.delete(body.followerId);
break; break;
} }
default: default:

View file

@ -47,6 +47,7 @@ import { SignupService } from './SignupService.js';
import { WebAuthnService } from './WebAuthnService.js'; import { WebAuthnService } from './WebAuthnService.js';
import { UserBlockingService } from './UserBlockingService.js'; import { UserBlockingService } from './UserBlockingService.js';
import { CacheService } from './CacheService.js'; import { CacheService } from './CacheService.js';
import { UserService } from './UserService.js';
import { UserFollowingService } from './UserFollowingService.js'; import { UserFollowingService } from './UserFollowingService.js';
import { UserKeypairService } from './UserKeypairService.js'; import { UserKeypairService } from './UserKeypairService.js';
import { UserListService } from './UserListService.js'; import { UserListService } from './UserListService.js';
@ -60,6 +61,8 @@ import { UtilityService } from './UtilityService.js';
import { FileInfoService } from './FileInfoService.js'; import { FileInfoService } from './FileInfoService.js';
import { SearchService } from './SearchService.js'; import { SearchService } from './SearchService.js';
import { ClipService } from './ClipService.js'; import { ClipService } from './ClipService.js';
import { FeaturedService } from './FeaturedService.js';
import { RedisTimelineService } from './RedisTimelineService.js';
import { ChartLoggerService } from './chart/ChartLoggerService.js'; import { ChartLoggerService } from './chart/ChartLoggerService.js';
import FederationChart from './chart/charts/federation.js'; import FederationChart from './chart/charts/federation.js';
import NotesChart from './chart/charts/notes.js'; import NotesChart from './chart/charts/notes.js';
@ -175,6 +178,7 @@ const $SignupService: Provider = { provide: 'SignupService', useExisting: Signup
const $WebAuthnService: Provider = { provide: 'WebAuthnService', useExisting: WebAuthnService }; const $WebAuthnService: Provider = { provide: 'WebAuthnService', useExisting: WebAuthnService };
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService }; const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService }; const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
const $UserService: Provider = { provide: 'UserService', useExisting: UserService };
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService }; const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService }; const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService }; const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
@ -187,6 +191,8 @@ const $UtilityService: Provider = { provide: 'UtilityService', useExisting: Util
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService }; const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService }; const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService }; const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
const $RedisTimelineService: Provider = { provide: 'RedisTimelineService', useExisting: RedisTimelineService };
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService }; const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart }; const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
@ -306,6 +312,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebAuthnService, WebAuthnService,
UserBlockingService, UserBlockingService,
CacheService, CacheService,
UserService,
UserFollowingService, UserFollowingService,
UserKeypairService, UserKeypairService,
UserListService, UserListService,
@ -318,6 +325,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
FileInfoService, FileInfoService,
SearchService, SearchService,
ClipService, ClipService,
FeaturedService,
RedisTimelineService,
ChartLoggerService, ChartLoggerService,
FederationChart, FederationChart,
NotesChart, NotesChart,
@ -430,6 +439,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$WebAuthnService, $WebAuthnService,
$UserBlockingService, $UserBlockingService,
$CacheService, $CacheService,
$UserService,
$UserFollowingService, $UserFollowingService,
$UserKeypairService, $UserKeypairService,
$UserListService, $UserListService,
@ -442,6 +452,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$FileInfoService, $FileInfoService,
$SearchService, $SearchService,
$ClipService, $ClipService,
$FeaturedService,
$RedisTimelineService,
$ChartLoggerService, $ChartLoggerService,
$FederationChart, $FederationChart,
$NotesChart, $NotesChart,
@ -555,6 +567,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebAuthnService, WebAuthnService,
UserBlockingService, UserBlockingService,
CacheService, CacheService,
UserService,
UserFollowingService, UserFollowingService,
UserKeypairService, UserKeypairService,
UserListService, UserListService,
@ -567,6 +580,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
FileInfoService, FileInfoService,
SearchService, SearchService,
ClipService, ClipService,
FeaturedService,
RedisTimelineService,
FederationChart, FederationChart,
NotesChart, NotesChart,
UsersChart, UsersChart,
@ -678,6 +693,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$WebAuthnService, $WebAuthnService,
$UserBlockingService, $UserBlockingService,
$CacheService, $CacheService,
$UserService,
$UserFollowingService, $UserFollowingService,
$UserKeypairService, $UserKeypairService,
$UserListService, $UserListService,
@ -690,6 +706,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$FileInfoService, $FileInfoService,
$SearchService, $SearchService,
$ClipService, $ClipService,
$FeaturedService,
$RedisTimelineService,
$FederationChart, $FederationChart,
$NotesChart, $NotesChart,
$UsersChart, $UsersChart,

View file

@ -52,7 +52,6 @@ export class CustomEmojiService implements OnApplicationShutdown {
fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))), fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))),
toRedisConverter: (value) => JSON.stringify(Array.from(value.values())), toRedisConverter: (value) => JSON.stringify(Array.from(value.values())),
fromRedisConverter: (value) => { fromRedisConverter: (value) => {
if (!Array.isArray(JSON.parse(value))) return undefined; // 古いバージョンの壊れたキャッシュが残っていることがある(そのうち消す)
return new Map(JSON.parse(value).map((x: Serialized<MiEmoji>) => [x.name, { return new Map(JSON.parse(value).map((x: Serialized<MiEmoji>) => [x.name, {
...x, ...x,
updatedAt: x.updatedAt ? new Date(x.updatedAt) : null, updatedAt: x.updatedAt ? new Date(x.updatedAt) : null,
@ -386,6 +385,20 @@ export class CustomEmojiService implements OnApplicationShutdown {
} }
} }
/**
*
* @param name
*/
@bindThis
public checkDuplicate(name: string): Promise<boolean> {
return this.emojisRepository.exist({ where: { name, host: IsNull() } });
}
@bindThis
public getEmojiById(id: string): Promise<MiEmoji | null> {
return this.emojisRepository.findOneBy({ id });
}
@bindThis @bindThis
public dispose(): void { public dispose(): void {
this.cache.dispose(); this.cache.dispose();

View file

@ -0,0 +1,116 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import type { MiNote, MiUser } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
const GLOBAL_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと
const PER_USER_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 7; // 1週間ごと
const HASHTAG_RANKING_WINDOW = 1000 * 60 * 60; // 1時間ごと
@Injectable()
export class FeaturedService {
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする
) {
}
@bindThis
private getCurrentWindow(windowRange: number): number {
const passed = new Date().getTime() - new Date(new Date().getFullYear(), 0, 1).getTime();
return Math.floor(passed / windowRange);
}
@bindThis
private async updateRankingOf(name: string, windowRange: number, element: string, score = 1): Promise<void> {
const currentWindow = this.getCurrentWindow(windowRange);
const redisTransaction = this.redisClient.multi();
redisTransaction.zincrby(
`${name}:${currentWindow}`,
score,
element);
redisTransaction.expire(
`${name}:${currentWindow}`,
(windowRange * 3) / 1000,
'NX'); // "NX -- Set expiry only when the key has no expiry" = 有効期限がないときだけ設定
await redisTransaction.exec();
}
@bindThis
private async getRankingOf(name: string, windowRange: number, threshold: number): Promise<string[]> {
const currentWindow = this.getCurrentWindow(windowRange);
const previousWindow = currentWindow - 1;
const redisPipeline = this.redisClient.pipeline();
redisPipeline.zrange(
`${name}:${currentWindow}`, 0, threshold, 'REV', 'WITHSCORES');
redisPipeline.zrange(
`${name}:${previousWindow}`, 0, threshold, 'REV', 'WITHSCORES');
const [currentRankingResult, previousRankingResult] = await redisPipeline.exec().then(result => result ? result.map(r => r[1] as string[]) : [[], []]);
const ranking = new Map<string, number>();
for (let i = 0; i < currentRankingResult.length; i += 2) {
const noteId = currentRankingResult[i];
const score = parseInt(currentRankingResult[i + 1], 10);
ranking.set(noteId, score);
}
for (let i = 0; i < previousRankingResult.length; i += 2) {
const noteId = previousRankingResult[i];
const score = parseInt(previousRankingResult[i + 1], 10);
const exist = ranking.get(noteId);
if (exist != null) {
ranking.set(noteId, (exist + score) / 2);
} else {
ranking.set(noteId, score);
}
}
return Array.from(ranking.keys());
}
@bindThis
public updateGlobalNotesRanking(noteId: MiNote['id'], score = 1): Promise<void> {
return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
}
@bindThis
public updateInChannelNotesRanking(channelId: MiNote['channelId'], noteId: MiNote['id'], score = 1): Promise<void> {
return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
}
@bindThis
public updatePerUserNotesRanking(userId: MiUser['id'], noteId: MiNote['id'], score = 1): Promise<void> {
return this.updateRankingOf(`featuredPerUserNotesRanking:${userId}`, PER_USER_NOTES_RANKING_WINDOW, noteId, score);
}
@bindThis
public updateHashtagsRanking(hashtag: string, score = 1): Promise<void> {
return this.updateRankingOf('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, hashtag, score);
}
@bindThis
public getGlobalNotesRanking(threshold: number): Promise<MiNote['id'][]> {
return this.getRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, threshold);
}
@bindThis
public getInChannelNotesRanking(channelId: MiNote['channelId'], threshold: number): Promise<MiNote['id'][]> {
return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, threshold);
}
@bindThis
public getPerUserNotesRanking(userId: MiUser['id'], threshold: number): Promise<MiNote['id'][]> {
return this.getRankingOf(`featuredPerUserNotesRanking:${userId}`, PER_USER_NOTES_RANKING_WINDOW, threshold);
}
@bindThis
public getHashtagsRanking(threshold: number): Promise<string[]> {
return this.getRankingOf('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, threshold);
}
}

View file

@ -4,6 +4,7 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js';
@ -12,15 +13,22 @@ import type { MiHashtag } from '@/models/Hashtag.js';
import type { HashtagsRepository } from '@/models/_.js'; import type { HashtagsRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import { MetaService } from '@/core/MetaService.js';
@Injectable() @Injectable()
export class HashtagService { export class HashtagService {
constructor( constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする
@Inject(DI.hashtagsRepository) @Inject(DI.hashtagsRepository)
private hashtagsRepository: HashtagsRepository, private hashtagsRepository: HashtagsRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private featuredService: FeaturedService,
private idService: IdService, private idService: IdService,
private metaService: MetaService,
) { ) {
} }
@ -46,6 +54,9 @@ export class HashtagService {
public async updateHashtag(user: { id: MiUser['id']; host: MiUser['host']; }, tag: string, isUserAttached = false, inc = true) { public async updateHashtag(user: { id: MiUser['id']; host: MiUser['host']; }, tag: string, isUserAttached = false, inc = true) {
tag = normalizeForSearch(tag); tag = normalizeForSearch(tag);
// TODO: サンプリング
this.updateHashtagsRanking(tag, user.id);
const index = await this.hashtagsRepository.findOneBy({ name: tag }); const index = await this.hashtagsRepository.findOneBy({ name: tag });
if (index == null && !inc) return; if (index == null && !inc) return;
@ -144,4 +155,94 @@ export class HashtagService {
} }
} }
} }
@bindThis
public async updateHashtagsRanking(hashtag: string, userId: MiUser['id']): Promise<void> {
const instance = await this.metaService.fetch();
const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t));
if (hiddenTags.includes(hashtag)) return;
// YYYYMMDDHHmm (10分間隔)
const now = new Date();
now.setMinutes(Math.floor(now.getMinutes() / 10) * 10, 0, 0);
const window = `${now.getUTCFullYear()}${(now.getUTCMonth() + 1).toString().padStart(2, '0')}${now.getUTCDate().toString().padStart(2, '0')}${now.getUTCHours().toString().padStart(2, '0')}${now.getUTCMinutes().toString().padStart(2, '0')}`;
const exist = await this.redisClient.sismember(`hashtagUsers:${hashtag}`, userId);
if (exist === 1) return;
this.featuredService.updateHashtagsRanking(hashtag, 1);
const redisPipeline = this.redisClient.pipeline();
// チャート用
redisPipeline.pfadd(`hashtagUsers:${hashtag}:${window}`, userId);
redisPipeline.expire(`hashtagUsers:${hashtag}:${window}`,
60 * 60 * 24 * 3, // 3日間
'NX', // "NX -- Set expiry only when the key has no expiry" = 有効期限がないときだけ設定
);
// ユニークカウント用
// TODO: Bloom Filter を使うようにしても良さそう
redisPipeline.sadd(`hashtagUsers:${hashtag}`, userId);
redisPipeline.expire(`hashtagUsers:${hashtag}`,
60 * 60, // 1時間
'NX', // "NX -- Set expiry only when the key has no expiry" = 有効期限がないときだけ設定
);
redisPipeline.exec();
}
@bindThis
public async getChart(hashtag: string, range: number): Promise<number[]> {
const now = new Date();
now.setMinutes(Math.floor(now.getMinutes() / 10) * 10, 0, 0);
const redisPipeline = this.redisClient.pipeline();
for (let i = 0; i < range; i++) {
const window = `${now.getUTCFullYear()}${(now.getUTCMonth() + 1).toString().padStart(2, '0')}${now.getUTCDate().toString().padStart(2, '0')}${now.getUTCHours().toString().padStart(2, '0')}${now.getUTCMinutes().toString().padStart(2, '0')}`;
redisPipeline.pfcount(`hashtagUsers:${hashtag}:${window}`);
now.setMinutes(now.getMinutes() - (i * 10), 0, 0);
}
const result = await redisPipeline.exec();
if (result == null) return [];
return result.map(x => x[1]) as number[];
}
@bindThis
public async getCharts(hashtags: string[], range: number): Promise<Record<string, number[]>> {
const now = new Date();
now.setMinutes(Math.floor(now.getMinutes() / 10) * 10, 0, 0);
const redisPipeline = this.redisClient.pipeline();
for (let i = 0; i < range; i++) {
const window = `${now.getUTCFullYear()}${(now.getUTCMonth() + 1).toString().padStart(2, '0')}${now.getUTCDate().toString().padStart(2, '0')}${now.getUTCHours().toString().padStart(2, '0')}${now.getUTCMinutes().toString().padStart(2, '0')}`;
for (const hashtag of hashtags) {
redisPipeline.pfcount(`hashtagUsers:${hashtag}:${window}`);
}
now.setMinutes(now.getMinutes() - (i * 10), 0, 0);
}
const result = await redisPipeline.exec();
if (result == null) return {};
// key is hashtag
const charts = {} as Record<string, number[]>;
for (const hashtag of hashtags) {
charts[hashtag] = [];
}
for (let i = 0; i < range; i++) {
for (let j = 0; j < hashtags.length; j++) {
charts[hashtags[j]].push(result[(i * hashtags.length) + j][1] as number);
}
}
return charts;
}
} }

View file

@ -5,7 +5,7 @@
import { setImmediate } from 'node:timers/promises'; import { setImmediate } from 'node:timers/promises';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import { In, DataSource } from 'typeorm'; import { In, DataSource, IsNull, LessThan } from 'typeorm';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import RE2 from 're2'; import RE2 from 're2';
@ -14,7 +14,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
import { extractHashtags } from '@/misc/extract-hashtags.js'; import { extractHashtags } from '@/misc/extract-hashtags.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { MiNote } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js';
import type { ChannelsRepository, FollowingsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiApp } from '@/models/App.js'; import type { MiApp } from '@/models/App.js';
import { concat } from '@/misc/prelude/array.js'; import { concat } from '@/misc/prelude/array.js';
@ -53,8 +53,8 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js'; import { SearchService } from '@/core/SearchService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
const mutedWordsCache = new MemorySingleCache<{ userId: MiUserProfile['userId']; mutedWords: MiUserProfile['mutedWords']; }[]>(1000 * 60 * 5); import { RedisTimelineService } from '@/core/RedisTimelineService.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -157,8 +157,8 @@ export class NoteCreateService implements OnApplicationShutdown {
@Inject(DI.db) @Inject(DI.db)
private db: DataSource, private db: DataSource,
@Inject(DI.redis) @Inject(DI.redisForTimelines)
private redisClient: Redis.Redis, private redisForTimelines: Redis.Redis,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -175,8 +175,8 @@ export class NoteCreateService implements OnApplicationShutdown {
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
@Inject(DI.mutedNotesRepository) @Inject(DI.userListMembershipsRepository)
private mutedNotesRepository: MutedNotesRepository, private userListMembershipsRepository: UserListMembershipsRepository,
@Inject(DI.channelsRepository) @Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository, private channelsRepository: ChannelsRepository,
@ -187,11 +187,15 @@ export class NoteCreateService implements OnApplicationShutdown {
@Inject(DI.followingsRepository) @Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository, private followingsRepository: FollowingsRepository,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private idService: IdService, private idService: IdService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private queueService: QueueService, private queueService: QueueService,
private redisTimelineService: RedisTimelineService,
private noteReadService: NoteReadService, private noteReadService: NoteReadService,
private notificationService: NotificationService, private notificationService: NotificationService,
private relayService: RelayService, private relayService: RelayService,
@ -199,6 +203,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private hashtagService: HashtagService, private hashtagService: HashtagService,
private antennaService: AntennaService, private antennaService: AntennaService,
private webhookService: WebhookService, private webhookService: WebhookService,
private featuredService: FeaturedService,
private remoteUserResolveService: RemoteUserResolveService, private remoteUserResolveService: RemoteUserResolveService,
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
@ -251,19 +256,30 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
} }
// Renote対象が「ホームまたは全体」以外の公開範囲ならreject if (data.renote) {
if (data.renote && data.renote.visibility !== 'public' && data.renote.visibility !== 'home' && data.renote.userId !== user.id) { switch (data.renote.visibility) {
case 'public':
// public noteは無条件にrenote可能
break;
case 'home':
// home noteはhome以下にrenote可能
if (data.visibility === 'public') {
data.visibility = 'home';
}
break;
case 'followers':
// 他人のfollowers noteはreject
if (data.renote.userId !== user.id) {
throw new Error('Renote target is not public or home'); throw new Error('Renote target is not public or home');
} }
// Renote対象がpublicではないならhomeにする
if (data.renote && data.renote.visibility !== 'public' && data.visibility === 'public') {
data.visibility = 'home';
}
// Renote対象がfollowersならfollowersにする // Renote対象がfollowersならfollowersにする
if (data.renote && data.renote.visibility === 'followers') {
data.visibility = 'followers'; data.visibility = 'followers';
break;
case 'specified':
// specified / direct noteはreject
throw new Error('Renote target is not public or home');
}
} }
// 返信対象がpublicではないならhomeにする // 返信対象がpublicではないならhomeにする
@ -333,14 +349,6 @@ export class NoteCreateService implements OnApplicationShutdown {
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers); const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
if (data.channel) {
this.redisClient.xadd(
`channelTimeline:${data.channel.id}`,
'MAXLEN', '~', this.config.perChannelMaxNoteCacheCount.toString(),
'*',
'note', note.id);
}
setImmediate('post created', { signal: this.#shutdownController.signal }).then( setImmediate('post created', { signal: this.#shutdownController.signal }).then(
() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!), () => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
() => { /* aborted, ignore this */ }, () => { /* aborted, ignore this */ },
@ -480,26 +488,7 @@ export class NoteCreateService implements OnApplicationShutdown {
// Increment notes count (user) // Increment notes count (user)
this.incNotesCountOfUser(user); this.incNotesCountOfUser(user);
// Word mute this.pushToTl(note, user);
mutedWordsCache.fetch(() => this.userProfilesRepository.find({
where: {
enableWordMute: true,
},
select: ['userId', 'mutedWords'],
})).then(us => {
for (const u of us) {
checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => {
if (shouldMute) {
this.mutedNotesRepository.insert({
id: this.idService.genId(),
userId: u.userId,
noteId: note.id,
reason: 'word',
});
}
});
}
});
this.antennaService.addNoteToAntennas(note, user); this.antennaService.addNoteToAntennas(note, user);
@ -508,11 +497,13 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
if (data.reply == null) { if (data.reply == null) {
// TODO: キャッシュ
this.followingsRepository.findBy({ this.followingsRepository.findBy({
followeeId: user.id, followeeId: user.id,
notify: 'normal', notify: 'normal',
}).then(followings => { }).then(followings => {
for (const following of followings) { for (const following of followings) {
// TODO: ワードミュート考慮
this.notificationService.createNotification(following.followerId, 'note', { this.notificationService.createNotification(following.followerId, 'note', {
noteId: note.id, noteId: note.id,
}, user.id); }, user.id);
@ -520,9 +511,8 @@ export class NoteCreateService implements OnApplicationShutdown {
}); });
} }
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき if (data.renote && data.renote.userId !== user.id && !user.isBot) {
if (data.renote && (await this.noteEntityService.countSameRenotes(user.id, data.renote.id, note.id) === 0)) { this.incRenoteCount(data.renote);
if (!user.isBot) this.incRenoteCount(data.renote);
} }
if (data.poll && data.poll.expiresAt) { if (data.poll && data.poll.expiresAt) {
@ -722,10 +712,23 @@ export class NoteCreateService implements OnApplicationShutdown {
this.notesRepository.createQueryBuilder().update() this.notesRepository.createQueryBuilder().update()
.set({ .set({
renoteCount: () => '"renoteCount" + 1', renoteCount: () => '"renoteCount" + 1',
score: () => '"score" + 1',
}) })
.where('id = :id', { id: renote.id }) .where('id = :id', { id: renote.id })
.execute(); .execute();
// 30%の確率、3日以内に投稿されたートの場合ハイライト用ランキング更新
if (Math.random() < 0.3 && (Date.now() - this.idService.parse(renote.id).date.getTime()) < 1000 * 60 * 60 * 24 * 3) {
if (renote.channelId != null) {
if (renote.replyId == null) {
this.featuredService.updateInChannelNotesRanking(renote.channelId, renote.id, 5);
}
} else {
if (renote.visibility === 'public' && renote.userHost == null && renote.replyId == null) {
this.featuredService.updateGlobalNotesRanking(renote.id, 5);
this.featuredService.updatePerUserNotesRanking(renote.userId, renote.id, 5);
}
}
}
} }
@bindThis @bindThis
@ -811,6 +814,165 @@ export class NoteCreateService implements OnApplicationShutdown {
return mentionedUsers; return mentionedUsers;
} }
@bindThis
private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
const meta = await this.metaService.fetch();
const r = this.redisForTimelines.pipeline();
if (note.channelId) {
this.redisTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r);
this.redisTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
const channelFollowings = await this.channelFollowingsRepository.find({
where: {
followeeId: note.channelId,
},
select: ['followerId'],
});
for (const channelFollowing of channelFollowings) {
this.redisTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.redisTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
}
}
} else {
// TODO: キャッシュ?
// eslint-disable-next-line prefer-const
let [followings, userListMemberships] = await Promise.all([
this.followingsRepository.find({
where: {
followeeId: user.id,
followerHost: IsNull(),
isFollowerHibernated: false,
},
select: ['followerId', 'withReplies'],
}),
this.userListMembershipsRepository.find({
where: {
userId: user.id,
},
select: ['userListId', 'userListUserId', 'withReplies'],
}),
]);
if (note.visibility === 'followers') {
// TODO: 重そうだから何とかしたい Set 使う?
userListMemberships = userListMemberships.filter(x => followings.some(f => f.followerId === x.userListUserId));
}
// TODO: あまりにも数が多いと redisPipeline.exec に失敗する(理由は不明)ため、3万件程度を目安に分割して実行するようにする
for (const following of followings) {
// 基本的にvisibleUserIdsには自身のidが含まれている前提であること
if (note.visibility === 'specified' && !note.visibleUserIds.some(v => v === following.followerId)) continue;
// 「自分自身への返信 or そのフォロワーへの返信」のどちらでもない場合
if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === following.followerId)) {
if (!following.withReplies) continue;
}
this.redisTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.redisTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
}
}
for (const userListMembership of userListMemberships) {
// ダイレクトのとき、そのリストが対象外のユーザーの場合
if (
note.visibility === 'specified' &&
!note.visibleUserIds.some(v => v === userListMembership.userListUserId)
) continue;
// 「自分自身への返信 or そのリストの作成者への返信」のどちらでもない場合
if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === userListMembership.userListUserId)) {
if (!userListMembership.withReplies) continue;
}
this.redisTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.redisTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r);
}
}
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL
this.redisTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.redisTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
}
}
// 自分自身以外への返信
if (note.replyId && note.replyUserId !== note.userId) {
this.redisTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
if (note.visibility === 'public' && note.userHost == null) {
this.redisTimelineService.push('localTimelineWithReplies', note.id, 300, r);
}
} else {
this.redisTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.redisTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r);
}
if (note.visibility === 'public' && note.userHost == null) {
this.redisTimelineService.push('localTimeline', note.id, 1000, r);
if (note.fileIds.length > 0) {
this.redisTimelineService.push('localTimelineWithFiles', note.id, 500, r);
}
}
}
if (Math.random() < 0.1) {
process.nextTick(() => {
this.checkHibernation(followings);
});
}
}
r.exec();
}
@bindThis
public async checkHibernation(followings: MiFollowing[]) {
if (followings.length === 0) return;
const shuffle = (array: MiFollowing[]) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
};
// ランダムに最大1000件サンプリング
const samples = shuffle(followings).slice(0, Math.min(followings.length, 1000));
const hibernatedUsers = await this.usersRepository.find({
where: {
id: In(samples.map(x => x.followerId)),
lastActiveDate: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 50))),
},
select: ['id'],
});
if (hibernatedUsers.length > 0) {
this.usersRepository.update({
id: In(hibernatedUsers.map(x => x.id)),
}, {
isHibernated: true,
});
this.followingsRepository.update({
followerId: In(hibernatedUsers.map(x => x.id)),
}, {
isFollowerHibernated: true,
});
}
}
@bindThis @bindThis
public dispose(): void { public dispose(): void {
this.#shutdownController.abort(); this.#shutdownController.abort();

View file

@ -64,12 +64,6 @@ export class NoteDeleteService {
const deletedAt = new Date(); const deletedAt = new Date();
const cascadingNotes = await this.findCascadingNotes(note); const cascadingNotes = await this.findCascadingNotes(note);
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
if (note.renoteId && (await this.noteEntityService.countSameRenotes(user.id, note.renoteId, note.id)) === 0) {
this.notesRepository.decrement({ id: note.renoteId }, 'renoteCount', 1);
if (!user.isBot) this.notesRepository.decrement({ id: note.renoteId }, 'score', 1);
}
if (note.replyId) { if (note.replyId) {
await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1); await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1);
} }

View file

@ -5,7 +5,7 @@
import { setImmediate } from 'node:timers/promises'; import { setImmediate } from 'node:timers/promises';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import { DataSource, In, LessThan } from 'typeorm'; import { DataSource, In, IsNull, LessThan } from 'typeorm';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import RE2 from 're2'; import RE2 from 're2';
@ -14,7 +14,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
import { extractHashtags } from '@/misc/extract-hashtags.js'; import { extractHashtags } from '@/misc/extract-hashtags.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { MiNote } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js';
import type { NoteEditRepository, ChannelsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { NoteEditRepository, ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiApp } from '@/models/App.js'; import type { MiApp } from '@/models/App.js';
import { concat } from '@/misc/prelude/array.js'; import { concat } from '@/misc/prelude/array.js';
@ -48,8 +48,11 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js'; import { SearchService } from '@/core/SearchService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
const mutedWordsCache = new MemorySingleCache<{ userId: MiUserProfile['userId']; mutedWords: MiUserProfile['mutedWords']; }[]>(1000 * 60 * 5); import { RedisTimelineService } from '@/core/RedisTimelineService.js';
import { AntennaService } from './AntennaService.js';
import NotesChart from './chart/charts/notes.js';
import PerUserNotesChart from './chart/charts/per-user-notes.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -151,12 +154,12 @@ export class NoteEditService implements OnApplicationShutdown {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.db) @Inject(DI.db)
private db: DataSource, private db: DataSource,
@Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -172,15 +175,21 @@ export class NoteEditService implements OnApplicationShutdown {
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
@Inject(DI.userListMembershipsRepository)
private userListMembershipsRepository: UserListMembershipsRepository,
@Inject(DI.channelsRepository) @Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository, private channelsRepository: ChannelsRepository,
@Inject(DI.mutedNotesRepository)
private mutedNotesRepository: MutedNotesRepository,
@Inject(DI.noteThreadMutingsRepository) @Inject(DI.noteThreadMutingsRepository)
private noteThreadMutingsRepository: NoteThreadMutingsRepository, private noteThreadMutingsRepository: NoteThreadMutingsRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
@Inject(DI.noteEditRepository) @Inject(DI.noteEditRepository)
private noteEditRepository: NoteEditRepository, private noteEditRepository: NoteEditRepository,
@ -189,18 +198,23 @@ export class NoteEditService implements OnApplicationShutdown {
private idService: IdService, private idService: IdService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private queueService: QueueService, private queueService: QueueService,
private redisTimelineService: RedisTimelineService,
private noteReadService: NoteReadService, private noteReadService: NoteReadService,
private notificationService: NotificationService, private notificationService: NotificationService,
private relayService: RelayService, private relayService: RelayService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private hashtagService: HashtagService, private hashtagService: HashtagService,
private antennaService: AntennaService,
private webhookService: WebhookService, private webhookService: WebhookService,
private featuredService: FeaturedService,
private remoteUserResolveService: RemoteUserResolveService, private remoteUserResolveService: RemoteUserResolveService,
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private roleService: RoleService, private roleService: RoleService,
private metaService: MetaService, private metaService: MetaService,
private searchService: SearchService, private searchService: SearchService,
private notesChart: NotesChart,
private perUserNotesChart: PerUserNotesChart,
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
) { } ) { }
@ -261,19 +275,30 @@ export class NoteEditService implements OnApplicationShutdown {
} }
} }
// Renote対象が「ホームまたは全体」以外の公開範囲ならreject if (data.renote) {
if (data.renote && data.renote.visibility !== 'public' && data.renote.visibility !== 'home' && data.renote.userId !== user.id) { switch (data.renote.visibility) {
case 'public':
// public noteは無条件にrenote可能
break;
case 'home':
// home noteはhome以下にrenote可能
if (data.visibility === 'public') {
data.visibility = 'home';
}
break;
case 'followers':
// 他人のfollowers noteはreject
if (data.renote.userId !== user.id) {
throw new Error('Renote target is not public or home'); throw new Error('Renote target is not public or home');
} }
// Renote対象がpublicではないならhomeにする
if (data.renote && data.renote.visibility !== 'public' && data.visibility === 'public') {
data.visibility = 'home';
}
// Renote対象がfollowersならfollowersにする // Renote対象がfollowersならfollowersにする
if (data.renote && data.renote.visibility === 'followers') {
data.visibility = 'followers'; data.visibility = 'followers';
break;
case 'specified':
// specified / direct noteはreject
throw new Error('Renote target is not public or home');
}
} }
// 返信対象がpublicではないならhomeにする // 返信対象がpublicではないならhomeにする
@ -448,14 +473,6 @@ export class NoteEditService implements OnApplicationShutdown {
await this.notesRepository.update(oldnote.id, note); await this.notesRepository.update(oldnote.id, note);
} }
if (data.channel) {
this.redisClient.xadd(
`channelTimeline:${data.channel.id}`,
'MAXLEN', '~', this.config.perChannelMaxNoteCacheCount.toString(),
'*',
'note', note.id);
}
setImmediate('post edited', { signal: this.#shutdownController.signal }).then( setImmediate('post edited', { signal: this.#shutdownController.signal }).then(
() => this.postNoteEdited(note, user, data, silent, tags!, mentionedUsers!), () => this.postNoteEdited(note, user, data, silent, tags!, mentionedUsers!),
() => { /* aborted, ignore this */ }, () => { /* aborted, ignore this */ },
@ -483,30 +500,7 @@ export class NoteEditService implements OnApplicationShutdown {
} }
// ハッシュタグ更新 // ハッシュタグ更新
if (data.visibility === 'public' || data.visibility === 'home') { this.pushToTl(note, user);
this.hashtagService.updateHashtags(user, tags);
}
// Word mute
mutedWordsCache.fetch(() => this.userProfilesRepository.find({
where: {
enableWordMute: true,
},
select: ['userId', 'mutedWords'],
})).then(us => {
for (const u of us) {
checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => {
if (shouldMute) {
this.mutedNotesRepository.insert({
id: this.idService.genId(),
userId: u.userId,
noteId: note.id,
reason: 'word',
});
}
});
}
});
if (data.poll && data.poll.expiresAt) { if (data.poll && data.poll.expiresAt) {
const delay = data.poll.expiresAt.getTime() - Date.now(); const delay = data.poll.expiresAt.getTime() - Date.now();
@ -779,6 +773,165 @@ export class NoteEditService implements OnApplicationShutdown {
return mentionedUsers; return mentionedUsers;
} }
@bindThis
private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
const meta = await this.metaService.fetch();
const r = this.redisForTimelines.pipeline();
if (note.channelId) {
this.redisTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r);
this.redisTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
const channelFollowings = await this.channelFollowingsRepository.find({
where: {
followeeId: note.channelId,
},
select: ['followerId'],
});
for (const channelFollowing of channelFollowings) {
this.redisTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.redisTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
}
}
} else {
// TODO: キャッシュ?
// eslint-disable-next-line prefer-const
let [followings, userListMemberships] = await Promise.all([
this.followingsRepository.find({
where: {
followeeId: user.id,
followerHost: IsNull(),
isFollowerHibernated: false,
},
select: ['followerId', 'withReplies'],
}),
this.userListMembershipsRepository.find({
where: {
userId: user.id,
},
select: ['userListId', 'userListUserId', 'withReplies'],
}),
]);
if (note.visibility === 'followers') {
// TODO: 重そうだから何とかしたい Set 使う?
userListMemberships = userListMemberships.filter(x => followings.some(f => f.followerId === x.userListUserId));
}
// TODO: あまりにも数が多いと redisPipeline.exec に失敗する(理由は不明)ため、3万件程度を目安に分割して実行するようにする
for (const following of followings) {
// 基本的にvisibleUserIdsには自身のidが含まれている前提であること
if (note.visibility === 'specified' && !note.visibleUserIds.some(v => v === following.followerId)) continue;
// 「自分自身への返信 or そのフォロワーへの返信」のどちらでもない場合
if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === following.followerId)) {
if (!following.withReplies) continue;
}
this.redisTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.redisTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
}
}
for (const userListMembership of userListMemberships) {
// ダイレクトのとき、そのリストが対象外のユーザーの場合
if (
note.visibility === 'specified' &&
!note.visibleUserIds.some(v => v === userListMembership.userListUserId)
) continue;
// 「自分自身への返信 or そのリストの作成者への返信」のどちらでもない場合
if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === userListMembership.userListUserId)) {
if (!userListMembership.withReplies) continue;
}
this.redisTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.redisTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r);
}
}
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL
this.redisTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.redisTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
}
}
// 自分自身以外への返信
if (note.replyId && note.replyUserId !== note.userId) {
this.redisTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
if (note.visibility === 'public' && note.userHost == null) {
this.redisTimelineService.push('localTimelineWithReplies', note.id, 300, r);
}
} else {
this.redisTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.redisTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r);
}
if (note.visibility === 'public' && note.userHost == null) {
this.redisTimelineService.push('localTimeline', note.id, 1000, r);
if (note.fileIds.length > 0) {
this.redisTimelineService.push('localTimelineWithFiles', note.id, 500, r);
}
}
}
if (Math.random() < 0.1) {
process.nextTick(() => {
this.checkHibernation(followings);
});
}
}
r.exec();
}
@bindThis
public async checkHibernation(followings: MiFollowing[]) {
if (followings.length === 0) return;
const shuffle = (array: MiFollowing[]) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
};
// ランダムに最大1000件サンプリング
const samples = shuffle(followings).slice(0, Math.min(followings.length, 1000));
const hibernatedUsers = await this.usersRepository.find({
where: {
id: In(samples.map(x => x.followerId)),
lastActiveDate: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 50))),
},
select: ['id'],
});
if (hibernatedUsers.length > 0) {
this.usersRepository.update({
id: In(hibernatedUsers.map(x => x.id)),
}, {
isHibernated: true,
});
this.followingsRepository.update({
followerId: In(hibernatedUsers.map(x => x.id)),
}, {
isFollowerHibernated: true,
});
}
}
@bindThis @bindThis
public dispose(): void { public dispose(): void {
this.#shutdownController.abort(); this.#shutdownController.abort();

View file

@ -99,19 +99,19 @@ export class NotificationService implements OnApplicationShutdown {
} }
if (recieveConfig?.type === 'following') { if (recieveConfig?.type === 'following') {
const isFollowing = await this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => followings.has(notifierId)); const isFollowing = await this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId));
if (!isFollowing) { if (!isFollowing) {
return null; return null;
} }
} else if (recieveConfig?.type === 'follower') { } else if (recieveConfig?.type === 'follower') {
const isFollower = await this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => followings.has(notifieeId)); const isFollower = await this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId));
if (!isFollower) { if (!isFollower) {
return null; return null;
} }
} else if (recieveConfig?.type === 'mutualFollow') { } else if (recieveConfig?.type === 'mutualFollow') {
const [isFollowing, isFollower] = await Promise.all([ const [isFollowing, isFollower] = await Promise.all([
this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => followings.has(notifierId)), this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)),
this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => followings.has(notifieeId)), this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)),
]); ]);
if (!isFollowing && !isFollower) { if (!isFollowing && !isFollower) {
return null; return null;

View file

@ -96,6 +96,8 @@ export class PollService {
const note = await this.notesRepository.findOneBy({ id: noteId }); const note = await this.notesRepository.findOneBy({ id: noteId });
if (note == null) throw new Error('note not found'); if (note == null) throw new Error('note not found');
if (note.localOnly) return;
const user = await this.usersRepository.findOneBy({ id: note.userId }); const user = await this.usersRepository.findOneBy({ id: note.userId });
if (user == null) throw new Error('note not found'); if (user == null) throw new Error('note not found');

View file

@ -7,8 +7,9 @@ import { Inject, Injectable } from '@nestjs/common';
import { Brackets, ObjectLiteral } from 'typeorm'; import { Brackets, ObjectLiteral } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/_.js'; import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import type { SelectQueryBuilder } from 'typeorm'; import type { SelectQueryBuilder } from 'typeorm';
@Injectable() @Injectable()
@ -23,9 +24,6 @@ export class QueryService {
@Inject(DI.channelFollowingsRepository) @Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository, private channelFollowingsRepository: ChannelFollowingsRepository,
@Inject(DI.mutedNotesRepository)
private mutedNotesRepository: MutedNotesRepository,
@Inject(DI.blockingsRepository) @Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository, private blockingsRepository: BlockingsRepository,
@ -37,6 +35,8 @@ export class QueryService {
@Inject(DI.renoteMutingsRepository) @Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository, private renoteMutingsRepository: RenoteMutingsRepository,
private idService: IdService,
) { ) {
} }
@ -52,15 +52,15 @@ export class QueryService {
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
q.orderBy(`${q.alias}.id`, 'DESC'); q.orderBy(`${q.alias}.id`, 'DESC');
} else if (sinceDate && untilDate) { } else if (sinceDate && untilDate) {
q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) }); q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: this.idService.genId(new Date(sinceDate)) });
q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) }); q.andWhere(`${q.alias}.id < :untilId`, { untilId: this.idService.genId(new Date(untilDate)) });
q.orderBy(`${q.alias}.createdAt`, 'DESC'); q.orderBy(`${q.alias}.id`, 'DESC');
} else if (sinceDate) { } else if (sinceDate) {
q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) }); q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: this.idService.genId(new Date(sinceDate)) });
q.orderBy(`${q.alias}.createdAt`, 'ASC'); q.orderBy(`${q.alias}.id`, 'ASC');
} else if (untilDate) { } else if (untilDate) {
q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) }); q.andWhere(`${q.alias}.id < :untilId`, { untilId: this.idService.genId(new Date(untilDate)) });
q.orderBy(`${q.alias}.createdAt`, 'DESC'); q.orderBy(`${q.alias}.id`, 'DESC');
} else { } else {
q.orderBy(`${q.alias}.id`, 'DESC'); q.orderBy(`${q.alias}.id`, 'DESC');
} }
@ -79,11 +79,13 @@ export class QueryService {
// 投稿の引用元の作者にブロックされていない // 投稿の引用元の作者にブロックされていない
q q
.andWhere(`note.userId NOT IN (${ blockingQuery.getQuery() })`) .andWhere(`note.userId NOT IN (${ blockingQuery.getQuery() })`)
.andWhere(new Brackets(qb => { qb .andWhere(new Brackets(qb => {
qb
.where('note.replyUserId IS NULL') .where('note.replyUserId IS NULL')
.orWhere(`note.replyUserId NOT IN (${ blockingQuery.getQuery() })`); .orWhere(`note.replyUserId NOT IN (${ blockingQuery.getQuery() })`);
})) }))
.andWhere(new Brackets(qb => { qb .andWhere(new Brackets(qb => {
qb
.where('note.renoteUserId IS NULL') .where('note.renoteUserId IS NULL')
.orWhere(`note.renoteUserId NOT IN (${ blockingQuery.getQuery() })`); .orWhere(`note.renoteUserId NOT IN (${ blockingQuery.getQuery() })`);
})); }));
@ -108,39 +110,6 @@ export class QueryService {
q.setParameters(blockedQuery.getParameters()); q.setParameters(blockedQuery.getParameters());
} }
@bindThis
public generateChannelQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void {
if (me == null) {
q.andWhere('note.channelId IS NULL');
} else {
q.leftJoinAndSelect('note.channel', 'channel');
const channelFollowingQuery = this.channelFollowingsRepository.createQueryBuilder('channelFollowing')
.select('channelFollowing.followeeId')
.where('channelFollowing.followerId = :followerId', { followerId: me.id });
q.andWhere(new Brackets(qb => { qb
// チャンネルのノートではない
.where('note.channelId IS NULL')
// または自分がフォローしているチャンネルのノート
.orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`);
}));
q.setParameters(channelFollowingQuery.getParameters());
}
}
@bindThis
public generateMutedNoteQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
const mutedQuery = this.mutedNotesRepository.createQueryBuilder('muted')
.select('muted.noteId')
.where('muted.userId = :userId', { userId: me.id });
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
q.setParameters(mutedQuery.getParameters());
}
@bindThis @bindThis
public generateMutedNoteThreadQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void { public generateMutedNoteThreadQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted') const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted')
@ -148,7 +117,8 @@ export class QueryService {
.where('threadMuted.userId = :userId', { userId: me.id }); .where('threadMuted.userId = :userId', { userId: me.id });
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
q.andWhere(new Brackets(qb => { qb q.andWhere(new Brackets(qb => {
qb
.where('note.threadId IS NULL') .where('note.threadId IS NULL')
.orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`); .orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`);
})); }));
@ -157,7 +127,7 @@ export class QueryService {
} }
@bindThis @bindThis
public generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }, exclude?: MiUser): void { public generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): void {
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
.select('muting.muteeId') .select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: me.id }); .where('muting.muterId = :muterId', { muterId: me.id });
@ -175,24 +145,29 @@ export class QueryService {
// 投稿の引用元の作者をミュートしていない // 投稿の引用元の作者をミュートしていない
q q
.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`) .andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`)
.andWhere(new Brackets(qb => { qb .andWhere(new Brackets(qb => {
qb
.where('note.replyUserId IS NULL') .where('note.replyUserId IS NULL')
.orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`); .orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`);
})) }))
.andWhere(new Brackets(qb => { qb .andWhere(new Brackets(qb => {
qb
.where('note.renoteUserId IS NULL') .where('note.renoteUserId IS NULL')
.orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`); .orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`);
})) }))
// mute instances // mute instances
.andWhere(new Brackets(qb => { qb .andWhere(new Brackets(qb => {
qb
.andWhere('note.userHost IS NULL') .andWhere('note.userHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`); .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`);
})) }))
.andWhere(new Brackets(qb => { qb .andWhere(new Brackets(qb => {
qb
.where('note.replyUserHost IS NULL') .where('note.replyUserHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`); .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`);
})) }))
.andWhere(new Brackets(qb => { qb .andWhere(new Brackets(qb => {
qb
.where('note.renoteUserHost IS NULL') .where('note.renoteUserHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`); .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
})); }));
@ -212,37 +187,12 @@ export class QueryService {
q.setParameters(mutingQuery.getParameters()); q.setParameters(mutingQuery.getParameters());
} }
@bindThis
public generateRepliesQuery(q: SelectQueryBuilder<any>, withReplies: boolean, me?: Pick<MiUser, 'id'> | null): void {
if (me == null) {
q.andWhere(new Brackets(qb => { qb
.where('note.replyId IS NULL') // 返信ではない
.orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = note.userId');
}));
}));
} else if (!withReplies) {
q.andWhere(new Brackets(qb => { qb
.where('note.replyId IS NULL') // 返信ではない
.orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信
.orWhere(new Brackets(qb => { qb // 返信だけど自分の行った返信
.where('note.replyId IS NOT NULL')
.andWhere('note.userId = :meId', { meId: me.id });
}))
.orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = note.userId');
}));
}));
}
}
@bindThis @bindThis
public generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void { public generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void {
// This code must always be synchronized with the checks in Notes.isVisibleForMe. // This code must always be synchronized with the checks in Notes.isVisibleForMe.
if (me == null) { if (me == null) {
q.andWhere(new Brackets(qb => { qb q.andWhere(new Brackets(qb => {
qb
.where('note.visibility = \'public\'') .where('note.visibility = \'public\'')
.orWhere('note.visibility = \'home\''); .orWhere('note.visibility = \'home\'');
})); }));
@ -251,9 +201,11 @@ export class QueryService {
.select('following.followeeId') .select('following.followeeId')
.where('following.followerId = :meId'); .where('following.followerId = :meId');
q.andWhere(new Brackets(qb => { qb q.andWhere(new Brackets(qb => {
qb
// 公開投稿である // 公開投稿である
.where(new Brackets(qb => { qb .where(new Brackets(qb => {
qb
.where('note.visibility = \'public\'') .where('note.visibility = \'public\'')
.orWhere('note.visibility = \'home\''); .orWhere('note.visibility = \'home\'');
})) }))
@ -262,10 +214,12 @@ export class QueryService {
// または 自分宛て // または 自分宛て
.orWhere(':meId = ANY(note.visibleUserIds)') .orWhere(':meId = ANY(note.visibleUserIds)')
.orWhere(':meId = ANY(note.mentions)') .orWhere(':meId = ANY(note.mentions)')
.orWhere(new Brackets(qb => { qb .orWhere(new Brackets(qb => {
qb
// または フォロワー宛ての投稿であり、 // または フォロワー宛ての投稿であり、
.where('note.visibility = \'followers\'') .where('note.visibility = \'followers\'')
.andWhere(new Brackets(qb => { qb .andWhere(new Brackets(qb => {
qb
// 自分がフォロワーである // 自分がフォロワーである
.where(`note.userId IN (${ followingQuery.getQuery() })`) .where(`note.userId IN (${ followingQuery.getQuery() })`)
// または 自分の投稿へのリプライ // または 自分の投稿へのリプライ

View file

@ -4,6 +4,7 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/_.js'; import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/_.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
@ -26,6 +27,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
const FALLBACK = '❤'; const FALLBACK = '❤';
@ -66,6 +68,9 @@ const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/;
@Injectable() @Injectable()
export class ReactionService { export class ReactionService {
constructor( constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -86,6 +91,7 @@ export class ReactionService {
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private userBlockingService: UserBlockingService, private userBlockingService: UserBlockingService,
private idService: IdService, private idService: IdService,
private featuredService: FeaturedService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
@ -182,11 +188,28 @@ export class ReactionService {
await this.notesRepository.createQueryBuilder().update() await this.notesRepository.createQueryBuilder().update()
.set({ .set({
reactions: () => sql, reactions: () => sql,
... (!user.isBot ? { score: () => '"score" + 1' } : {}),
}) })
.where('id = :id', { id: note.id }) .where('id = :id', { id: note.id })
.execute(); .execute();
// 30%の確率、セルフではない、3日以内に投稿されたートの場合ハイライト用ランキング更新
if (
Math.random() < 0.3 &&
note.userId !== user.id &&
(Date.now() - this.idService.parse(note.id).date.getTime()) < 1000 * 60 * 60 * 24 * 3
) {
if (note.channelId != null) {
if (note.replyId == null) {
this.featuredService.updateInChannelNotesRanking(note.channelId, note.id, 1);
}
} else {
if (note.visibility === 'public' && note.userHost == null && note.replyId == null) {
this.featuredService.updateGlobalNotesRanking(note.id, 1);
this.featuredService.updatePerUserNotesRanking(note.userId, note.id, 1);
}
}
}
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
if (meta.enableChartsForRemoteUser || (user.host == null)) { if (meta.enableChartsForRemoteUser || (user.host == null)) {
@ -275,8 +298,6 @@ export class ReactionService {
.where('id = :id', { id: note.id }) .where('id = :id', { id: note.id })
.execute(); .execute();
if (!user.isBot) this.notesRepository.decrement({ id: note.id }, 'score', 1);
this.globalEventService.publishNoteStream(note.id, 'unreacted', { this.globalEventService.publishNoteStream(note.id, 'unreacted', {
reaction: this.decodeReaction(exist.reaction).reaction, reaction: this.decodeReaction(exist.reaction).reaction,
userId: user.id, userId: user.id,

View file

@ -0,0 +1,80 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
@Injectable()
export class RedisTimelineService {
constructor(
@Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis,
private idService: IdService,
) {
}
@bindThis
public push(tl: string, id: string, maxlen: number, pipeline: Redis.ChainableCommander) {
// リモートから遅れて届いた(もしくは後から追加された)投稿日時が古い投稿が追加されるとページネーション時に問題を引き起こすため、
// 3分以内に投稿されたものでない場合、Redisにある最古のIDより新しい場合のみ追加する
if (this.idService.parse(id).date.getTime() > Date.now() - 1000 * 60 * 3) {
pipeline.lpush('list:' + tl, id);
if (Math.random() < 0.1) { // 10%の確率でトリム
pipeline.ltrim('list:' + tl, 0, maxlen - 1);
}
} else {
// 末尾のIDを取得
this.redisForTimelines.lindex('list:' + tl, -1).then(lastId => {
if (lastId == null || (this.idService.parse(id).date.getTime() > this.idService.parse(lastId).date.getTime())) {
this.redisForTimelines.lpush('list:' + tl, id);
} else {
Promise.resolve();
}
});
}
}
@bindThis
public get(name: string, untilId?: string | null, sinceId?: string | null) {
if (untilId && sinceId) {
return this.redisForTimelines.lrange('list:' + name, 0, -1)
.then(ids => ids.filter(id => id < untilId && id > sinceId).sort((a, b) => a > b ? -1 : 1));
} else if (untilId) {
return this.redisForTimelines.lrange('list:' + name, 0, -1)
.then(ids => ids.filter(id => id < untilId).sort((a, b) => a > b ? -1 : 1));
} else if (sinceId) {
return this.redisForTimelines.lrange('list:' + name, 0, -1)
.then(ids => ids.filter(id => id > sinceId).sort((a, b) => a < b ? -1 : 1));
} else {
return this.redisForTimelines.lrange('list:' + name, 0, -1)
.then(ids => ids.sort((a, b) => a > b ? -1 : 1));
}
}
@bindThis
public getMulti(name: string[], untilId?: string | null, sinceId?: string | null): Promise<string[][]> {
const pipeline = this.redisForTimelines.pipeline();
for (const n of name) {
pipeline.lrange('list:' + n, 0, -1);
}
return pipeline.exec().then(res => {
if (res == null) return [];
const tls = res.map(r => r[1] as string[]);
return tls.map(ids =>
(untilId && sinceId)
? ids.filter(id => id < untilId && id > sinceId).sort((a, b) => a > b ? -1 : 1)
: untilId
? ids.filter(id => id < untilId).sort((a, b) => a > b ? -1 : 1)
: sinceId
? ids.filter(id => id > sinceId).sort((a, b) => a < b ? -1 : 1)
: ids.sort((a, b) => a > b ? -1 : 1),
);
});
}
}

View file

@ -20,6 +20,7 @@ import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { RedisTimelineService } from '@/core/RedisTimelineService.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
export type RolePolicies = { export type RolePolicies = {
@ -102,6 +103,7 @@ export class RoleService implements OnApplicationShutdown {
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private idService: IdService, private idService: IdService,
private moderationLogService: ModerationLogService, private moderationLogService: ModerationLogService,
private redisTimelineService: RedisTimelineService,
) { ) {
//this.onMessage = this.onMessage.bind(this); //this.onMessage = this.onMessage.bind(this);
@ -472,12 +474,7 @@ export class RoleService implements OnApplicationShutdown {
const redisPipeline = this.redisClient.pipeline(); const redisPipeline = this.redisClient.pipeline();
for (const role of roles) { for (const role of roles) {
redisPipeline.xadd( this.redisTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline);
`roleTimeline:${role.id}`,
'MAXLEN', '~', '1000',
'*',
'note', note.id);
this.globalEventService.publishRoleTimelineStream(role.id, 'note', note); this.globalEventService.publishRoleTimelineStream(role.id, 'note', note);
} }

View file

@ -11,7 +11,7 @@ import type { MiBlocking } from '@/models/Blocking.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/_.js'; import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListMembershipsRepository } from '@/models/_.js';
import Logger from '@/logger.js'; import Logger from '@/logger.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
@ -38,8 +38,8 @@ export class UserBlockingService implements OnModuleInit {
@Inject(DI.userListsRepository) @Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository, private userListsRepository: UserListsRepository,
@Inject(DI.userListJoiningsRepository) @Inject(DI.userListMembershipsRepository)
private userListJoiningsRepository: UserListJoiningsRepository, private userListMembershipsRepository: UserListMembershipsRepository,
private cacheService: CacheService, private cacheService: CacheService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
@ -149,7 +149,7 @@ export class UserBlockingService implements OnModuleInit {
}); });
for (const userList of userLists) { for (const userList of userLists) {
await this.userListJoiningsRepository.delete({ await this.userListMembershipsRepository.delete({
userListId: userList.id, userListId: userList.id,
userId: user.id, userId: user.id,
}); });

View file

@ -123,7 +123,11 @@ export class UserFollowingService implements OnModuleInit {
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである // フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく // 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
if (followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee))) { if (
followee.isLocked ||
(followeeProfile.carefulBot && follower.isBot) ||
(this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true')
) {
let autoAccept = false; let autoAccept = false;
// 鍵アカウントであっても、既にフォローされていた場合はスルー // 鍵アカウントであっても、既にフォローされていた場合はスルー

View file

@ -5,10 +5,10 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import type { UserListJoiningsRepository } from '@/models/_.js'; import type { UserListMembershipsRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import type { MiUserList } from '@/models/UserList.js'; import type { MiUserList } from '@/models/UserList.js';
import type { MiUserListJoining } from '@/models/UserListJoining.js'; import type { MiUserListMembership } from '@/models/UserListMembership.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -33,8 +33,8 @@ export class UserListService implements OnApplicationShutdown {
@Inject(DI.redisForSub) @Inject(DI.redisForSub)
private redisForSub: Redis.Redis, private redisForSub: Redis.Redis,
@Inject(DI.userListJoiningsRepository) @Inject(DI.userListMembershipsRepository)
private userListJoiningsRepository: UserListJoiningsRepository, private userListMembershipsRepository: UserListMembershipsRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
@ -46,7 +46,7 @@ export class UserListService implements OnApplicationShutdown {
this.membersCache = new RedisKVCache<Set<string>>(this.redisClient, 'userListMembers', { this.membersCache = new RedisKVCache<Set<string>>(this.redisClient, 'userListMembers', {
lifetime: 1000 * 60 * 30, // 30m lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.userListJoiningsRepository.find({ where: { userListId: key }, select: ['userId'] }).then(xs => new Set(xs.map(x => x.userId))), fetcher: (key) => this.userListMembershipsRepository.find({ where: { userListId: key }, select: ['userId'] }).then(xs => new Set(xs.map(x => x.userId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)), toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)), fromRedisConverter: (value) => new Set(JSON.parse(value)),
}); });
@ -85,19 +85,20 @@ export class UserListService implements OnApplicationShutdown {
@bindThis @bindThis
public async addMember(target: MiUser, list: MiUserList, me: MiUser) { public async addMember(target: MiUser, list: MiUserList, me: MiUser) {
const currentCount = await this.userListJoiningsRepository.countBy({ const currentCount = await this.userListMembershipsRepository.countBy({
userListId: list.id, userListId: list.id,
}); });
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) { if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
throw new UserListService.TooManyUsersError(); throw new UserListService.TooManyUsersError();
} }
await this.userListJoiningsRepository.insert({ await this.userListMembershipsRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),
userId: target.id, userId: target.id,
userListId: list.id, userListId: list.id,
} as MiUserListJoining); userListUserId: list.userId,
} as MiUserListMembership);
this.globalEventService.publishInternalEvent('userListMemberAdded', { userListId: list.id, memberId: target.id }); this.globalEventService.publishInternalEvent('userListMemberAdded', { userListId: list.id, memberId: target.id });
this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target)); this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
@ -113,7 +114,7 @@ export class UserListService implements OnApplicationShutdown {
@bindThis @bindThis
public async removeMember(target: MiUser, list: MiUserList) { public async removeMember(target: MiUser, list: MiUserList) {
await this.userListJoiningsRepository.delete({ await this.userListMembershipsRepository.delete({
userId: target.id, userId: target.id,
userListId: list.id, userListId: list.id,
}); });
@ -122,6 +123,24 @@ export class UserListService implements OnApplicationShutdown {
this.globalEventService.publishUserListStream(list.id, 'userRemoved', await this.userEntityService.pack(target)); this.globalEventService.publishUserListStream(list.id, 'userRemoved', await this.userEntityService.pack(target));
} }
@bindThis
public async updateMembership(target: MiUser, list: MiUserList, options: { withReplies?: boolean }) {
const membership = await this.userListMembershipsRepository.findOneBy({
userId: target.id,
userListId: list.id,
});
if (membership == null) {
throw new Error('User is not a member of the list');
}
await this.userListMembershipsRepository.update({
id: membership.id,
}, {
withReplies: options.withReplies,
});
}
@bindThis @bindThis
public dispose(): void { public dispose(): void {
this.redisForSub.off('message', this.onMessage); this.redisForSub.off('message', this.onMessage);

View file

@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class UserService {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
) {
}
@bindThis
public async updateLastActiveDate(user: MiUser): Promise<void> {
if (user.isHibernated) {
const result = await this.usersRepository.createQueryBuilder().update()
.set({
lastActiveDate: new Date(),
})
.where('id = :id', { id: user.id })
.returning('*')
.execute()
.then((response) => {
return response.raw[0];
});
const wokeUp = result.isHibernated;
if (wokeUp) {
this.usersRepository.update(user.id, {
isHibernated: false,
});
this.followingsRepository.update({
followerId: user.id,
}, {
isFollowerHibernated: false,
});
}
} else {
this.usersRepository.update(user.id, {
lastActiveDate: new Date(),
});
}
}
}

View file

@ -17,6 +17,7 @@ import type { MiNoteReaction } from '@/models/NoteReaction.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js'; import { isNotNull } from '@/misc/is-not-null.js';
import { DebounceLoader } from '@/misc/loader.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { ReactionService } from '../ReactionService.js'; import type { ReactionService } from '../ReactionService.js';
@ -30,6 +31,7 @@ export class NoteEntityService implements OnModuleInit {
private driveFileEntityService: DriveFileEntityService; private driveFileEntityService: DriveFileEntityService;
private customEmojiService: CustomEmojiService; private customEmojiService: CustomEmojiService;
private reactionService: ReactionService; private reactionService: ReactionService;
private noteLoader = new DebounceLoader(this.findNoteOrFail);
constructor( constructor(
private moduleRef: ModuleRef, private moduleRef: ModuleRef,
@ -289,8 +291,8 @@ export class NoteEntityService implements OnModuleInit {
}, options); }, options);
const meId = me ? me.id : null; const meId = me ? me.id : null;
const note = typeof src === 'object' ? src : await this.notesRepository.findOneOrFail({ where: { id: src }, relations: ['user'] }); const note = typeof src === 'object' ? src : await this.noteLoader.load(src);
const host = note.userHost == null ? this.config.host : note.userHost; const host = note.userHost;
let text = note.text; let text = note.text;
@ -455,17 +457,10 @@ export class NoteEntityService implements OnModuleInit {
} }
@bindThis @bindThis
public async countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> { private findNoteOrFail(id: string): Promise<MiNote> {
// 指定したユーザーの指定したノートのリノートがいくつあるか数える return this.notesRepository.findOneOrFail({
const query = this.notesRepository.createQueryBuilder('note') where: { id },
.where('note.userId = :userId', { userId }) relations: ['user'],
.andWhere('note.renoteId = :renoteId', { renoteId }); });
// 指定した投稿を除く
if (excludeNoteId) {
query.andWhere('note.id != :excludeNoteId', { excludeNoteId });
}
return await query.getCount();
} }
} }

View file

@ -33,7 +33,8 @@ export class RoleEntityService {
const assignedCount = await this.roleAssignmentsRepository.createQueryBuilder('assign') const assignedCount = await this.roleAssignmentsRepository.createQueryBuilder('assign')
.where('assign.roleId = :roleId', { roleId: role.id }) .where('assign.roleId = :roleId', { roleId: role.id })
.andWhere(new Brackets(qb => { qb .andWhere(new Brackets(qb => {
qb
.where('assign.expiresAt IS NULL') .where('assign.expiresAt IS NULL')
.orWhere('assign.expiresAt > :now', { now: new Date() }); .orWhere('assign.expiresAt > :now', { now: new Date() });
})) }))

View file

@ -147,64 +147,76 @@ export class UserEntityService implements OnModuleInit {
@bindThis @bindThis
public async getRelation(me: MiUser['id'], target: MiUser['id']) { public async getRelation(me: MiUser['id'], target: MiUser['id']) {
const following = await this.followingsRepository.findOneBy({ const [
followerId: me,
followeeId: target,
});
return awaitAll({
id: target,
following, following,
isFollowing: following != null, isFollowed,
isFollowed: this.followingsRepository.count({ hasPendingFollowRequestFromYou,
hasPendingFollowRequestToYou,
isBlocking,
isBlocked,
isMuted,
isRenoteMuted,
] = await Promise.all([
this.followingsRepository.findOneBy({
followerId: me,
followeeId: target,
}),
this.followingsRepository.exist({
where: { where: {
followerId: target, followerId: target,
followeeId: me, followeeId: me,
}, },
take: 1, }),
}).then(n => n > 0), this.followRequestsRepository.exist({
hasPendingFollowRequestFromYou: this.followRequestsRepository.count({
where: { where: {
followerId: me, followerId: me,
followeeId: target, followeeId: target,
}, },
take: 1, }),
}).then(n => n > 0), this.followRequestsRepository.exist({
hasPendingFollowRequestToYou: this.followRequestsRepository.count({
where: { where: {
followerId: target, followerId: target,
followeeId: me, followeeId: me,
}, },
take: 1, }),
}).then(n => n > 0), this.blockingsRepository.exist({
isBlocking: this.blockingsRepository.count({
where: { where: {
blockerId: me, blockerId: me,
blockeeId: target, blockeeId: target,
}, },
take: 1, }),
}).then(n => n > 0), this.blockingsRepository.exist({
isBlocked: this.blockingsRepository.count({
where: { where: {
blockerId: target, blockerId: target,
blockeeId: me, blockeeId: me,
}, },
take: 1, }),
}).then(n => n > 0), this.mutingsRepository.exist({
isMuted: this.mutingsRepository.count({
where: { where: {
muterId: me, muterId: me,
muteeId: target, muteeId: target,
}, },
take: 1, }),
}).then(n => n > 0), this.renoteMutingsRepository.exist({
isRenoteMuted: this.renoteMutingsRepository.count({
where: { where: {
muterId: me, muterId: me,
muteeId: target, muteeId: target,
}, },
take: 1, }),
}).then(n => n > 0), ]);
});
return {
id: target,
following,
isFollowing: following != null,
isFollowed,
hasPendingFollowRequestFromYou,
hasPendingFollowRequestToYou,
isBlocking,
isBlocked,
isMuted,
isRenoteMuted,
};
} }
@bindThis @bindThis
@ -505,6 +517,7 @@ export class UserEntityService implements OnModuleInit {
isMuted: relation.isMuted, isMuted: relation.isMuted,
isRenoteMuted: relation.isRenoteMuted, isRenoteMuted: relation.isRenoteMuted,
notify: relation.following?.notify ?? 'none', notify: relation.following?.notify ?? 'none',
withReplies: relation.following?.withReplies ?? false,
} : {}), } : {}),
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>; } as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;

View file

@ -5,11 +5,12 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { UserListJoiningsRepository, UserListsRepository } from '@/models/_.js'; import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import type { } from '@/models/Blocking.js'; import type { } from '@/models/Blocking.js';
import type { MiUserList } from '@/models/UserList.js'; import type { MiUserList } from '@/models/UserList.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable() @Injectable()
export class UserListEntityService { export class UserListEntityService {
@ -17,8 +18,10 @@ export class UserListEntityService {
@Inject(DI.userListsRepository) @Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository, private userListsRepository: UserListsRepository,
@Inject(DI.userListJoiningsRepository) @Inject(DI.userListMembershipsRepository)
private userListJoiningsRepository: UserListJoiningsRepository, private userListMembershipsRepository: UserListMembershipsRepository,
private userEntityService: UserEntityService,
) { ) {
} }
@ -28,7 +31,7 @@ export class UserListEntityService {
): Promise<Packed<'UserList'>> { ): Promise<Packed<'UserList'>> {
const userList = typeof src === 'object' ? src : await this.userListsRepository.findOneByOrFail({ id: src }); const userList = typeof src === 'object' ? src : await this.userListsRepository.findOneByOrFail({ id: src });
const users = await this.userListJoiningsRepository.findBy({ const users = await this.userListMembershipsRepository.findBy({
userListId: userList.id, userListId: userList.id,
}); });
@ -40,5 +43,18 @@ export class UserListEntityService {
isPublic: userList.isPublic, isPublic: userList.isPublic,
}; };
} }
@bindThis
public async packMembershipsMany(
memberships: MiUserListMembership[],
) {
return Promise.all(memberships.map(async x => ({
id: x.id,
createdAt: x.createdAt.toISOString(),
userId: x.userId,
user: await this.userEntityService.pack(x.userId),
withReplies: x.withReplies,
})));
}
} }

View file

@ -10,6 +10,7 @@ export const DI = {
redis: Symbol('redis'), redis: Symbol('redis'),
redisForPub: Symbol('redisForPub'), redisForPub: Symbol('redisForPub'),
redisForSub: Symbol('redisForSub'), redisForSub: Symbol('redisForSub'),
redisForTimelines: Symbol('redisForTimelines'),
//#region Repositories //#region Repositories
usersRepository: Symbol('usersRepository'), usersRepository: Symbol('usersRepository'),
@ -30,7 +31,7 @@ export const DI = {
userPublickeysRepository: Symbol('userPublickeysRepository'), userPublickeysRepository: Symbol('userPublickeysRepository'),
userListsRepository: Symbol('userListsRepository'), userListsRepository: Symbol('userListsRepository'),
userListFavoritesRepository: Symbol('userListFavoritesRepository'), userListFavoritesRepository: Symbol('userListFavoritesRepository'),
userListJoiningsRepository: Symbol('userListJoiningsRepository'), userListMembershipsRepository: Symbol('userListMembershipsRepository'),
userNotePiningsRepository: Symbol('userNotePiningsRepository'), userNotePiningsRepository: Symbol('userNotePiningsRepository'),
userIpsRepository: Symbol('userIpsRepository'), userIpsRepository: Symbol('userIpsRepository'),
usedUsernamesRepository: Symbol('usedUsernamesRepository'), usedUsernamesRepository: Symbol('usedUsernamesRepository'),
@ -63,7 +64,6 @@ export const DI = {
promoNotesRepository: Symbol('promoNotesRepository'), promoNotesRepository: Symbol('promoNotesRepository'),
promoReadsRepository: Symbol('promoReadsRepository'), promoReadsRepository: Symbol('promoReadsRepository'),
relaysRepository: Symbol('relaysRepository'), relaysRepository: Symbol('relaysRepository'),
mutedNotesRepository: Symbol('mutedNotesRepository'),
channelsRepository: Symbol('channelsRepository'), channelsRepository: Symbol('channelsRepository'),
channelFollowingsRepository: Symbol('channelFollowingsRepository'), channelFollowingsRepository: Symbol('channelFollowingsRepository'),
channelFavoritesRepository: Symbol('channelFavoritesRepository'), channelFavoritesRepository: Symbol('channelFavoritesRepository'),

View file

@ -3,16 +3,16 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
export function isUserRelated(note: any, userIds: Set<string>): boolean { export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean {
if (userIds.has(note.userId)) { if (userIds.has(note.userId) && !ignoreAuthor) {
return true; return true;
} }
if (note.reply != null && userIds.has(note.reply.userId)) { if (note.reply != null && note.reply.userId !== note.userId && userIds.has(note.reply.userId)) {
return true; return true;
} }
if (note.renote != null && userIds.has(note.renote.userId)) { if (note.renote != null && note.renote.userId !== note.userId && userIds.has(note.renote.userId)) {
return true; return true;
} }

View file

@ -0,0 +1,52 @@
export type FetchFunction<K, V> = (key: K) => Promise<V>;
type ResolveReject<V> = Parameters<ConstructorParameters<typeof Promise<V>>[0]>;
type ResolverPair<V> = {
resolve: ResolveReject<V>[0];
reject: ResolveReject<V>[1];
};
export class DebounceLoader<K, V> {
private resolverMap = new Map<K, ResolverPair<V>>();
private promiseMap = new Map<K, Promise<V>>();
private resolvedPromise = Promise.resolve();
constructor(private loadFn: FetchFunction<K, V>) {}
public load(key: K): Promise<V> {
const promise = this.promiseMap.get(key);
if (typeof promise !== 'undefined') {
return promise;
}
const isFirst = this.promiseMap.size === 0;
const newPromise = new Promise<V>((resolve, reject) => {
this.resolverMap.set(key, { resolve, reject });
});
this.promiseMap.set(key, newPromise);
if (isFirst) {
this.enqueueDebouncedLoadJob();
}
return newPromise;
}
private runDebouncedLoad(): void {
const resolvers = [...this.resolverMap];
this.resolverMap.clear();
this.promiseMap.clear();
for (const [key, { resolve, reject }] of resolvers) {
this.loadFn(key).then(resolve, reject);
}
}
private enqueueDebouncedLoadJob(): void {
this.resolvedPromise.then(() => {
process.nextTick(() => {
this.runDebouncedLoad();
});
});
}
}

View file

@ -9,6 +9,7 @@ import { MiUser } from './User.js';
@Entity('following') @Entity('following')
@Index(['followerId', 'followeeId'], { unique: true }) @Index(['followerId', 'followeeId'], { unique: true })
@Index(['followeeId', 'followerHost', 'isFollowerHibernated'])
export class MiFollowing { export class MiFollowing {
@PrimaryColumn(id()) @PrimaryColumn(id())
public id: string; public id: string;
@ -45,6 +46,17 @@ export class MiFollowing {
@JoinColumn() @JoinColumn()
public follower: MiUser | null; public follower: MiUser | null;
@Column('boolean', {
default: false,
})
public isFollowerHibernated: boolean;
// タイムラインにその人のリプライまで含めるかどうか
@Column('boolean', {
default: false,
})
public withReplies: boolean;
@Index() @Index()
@Column('varchar', { @Column('varchar', {
length: 32, length: 32,

View file

@ -335,6 +335,18 @@ export class MiMeta {
}) })
public feedbackUrl: string | null; public feedbackUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public impressumUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public privacyPolicyUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 8192, length: 8192,
nullable: true, nullable: true,
@ -476,4 +488,29 @@ export class MiMeta {
length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }', length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }',
}) })
public preservedUsernames: string[]; public preservedUsernames: string[];
@Column('integer', {
default: 300,
})
public perLocalUserUserTimelineCacheMax: number;
@Column('integer', {
default: 100,
})
public perRemoteUserUserTimelineCacheMax: number;
@Column('integer', {
default: 300,
})
public perUserHomeTimelineCacheMax: number;
@Column('integer', {
default: 300,
})
public perUserListTimelineCacheMax: number;
@Column('integer', {
default: 0,
})
public notesPerOneAd: number;
} }

View file

@ -1,53 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
import { mutedNoteReasons } from '@/types.js';
import { id } from './util/id.js';
import { MiNote } from './Note.js';
import { MiUser } from './User.js';
@Entity('muted_note')
@Index(['noteId', 'userId'], { unique: true })
export class MiMutedNote {
@PrimaryColumn(id())
public id: string;
@Index()
@Column({
...id(),
comment: 'The note ID.',
})
public noteId: MiNote['id'];
@ManyToOne(type => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
public note: MiNote | null;
@Index()
@Column({
...id(),
comment: 'The user ID.',
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: MiUser | null;
/**
*
*/
@Index()
@Column('enum', {
enum: mutedNoteReasons,
comment: 'The reason of the MutedNote.',
})
public reason: typeof mutedNoteReasons[number];
}

View file

@ -18,7 +18,6 @@ export class MiNote {
@PrimaryColumn(id()) @PrimaryColumn(id())
public id: string; public id: string;
@Index()
@Column('timestamp with time zone', { @Column('timestamp with time zone', {
comment: 'The created date of the Note.', comment: 'The created date of the Note.',
}) })
@ -145,11 +144,6 @@ export class MiNote {
}) })
public url: string | null; public url: string | null;
@Column('integer', {
default: 0, select: false,
})
public score: number;
@Index() @Index()
@Column({ @Column({
...id(), ...id(),
@ -157,7 +151,6 @@ export class MiNote {
}) })
public fileIds: MiDriveFile['id'][]; public fileIds: MiDriveFile['id'][];
@Index()
@Column('varchar', { @Column('varchar', {
length: 256, array: true, default: '{}', length: 256, array: true, default: '{}',
}) })

View file

@ -14,7 +14,6 @@ export class MiNoteReaction {
@PrimaryColumn(id()) @PrimaryColumn(id())
public id: string; public id: string;
@Index()
@Column('timestamp with time zone', { @Column('timestamp with time zone', {
comment: 'The created date of the NoteReaction.', comment: 'The created date of the NoteReaction.',
}) })

View file

@ -5,7 +5,7 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMutedNote, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListJoining, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, NoteEdit } from './_.js'; import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, NoteEdit } from './_.js';
import type { DataSource } from 'typeorm'; import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common'; import type { Provider } from '@nestjs/common';
@ -117,9 +117,9 @@ const $userListFavoritesRepository: Provider = {
inject: [DI.db], inject: [DI.db],
}; };
const $userListJoiningsRepository: Provider = { const $userListMembershipsRepository: Provider = {
provide: DI.userListJoiningsRepository, provide: DI.userListMembershipsRepository,
useFactory: (db: DataSource) => db.getRepository(MiUserListJoining), useFactory: (db: DataSource) => db.getRepository(MiUserListMembership),
inject: [DI.db], inject: [DI.db],
}; };
@ -315,12 +315,6 @@ const $relaysRepository: Provider = {
inject: [DI.db], inject: [DI.db],
}; };
const $mutedNotesRepository: Provider = {
provide: DI.mutedNotesRepository,
useFactory: (db: DataSource) => db.getRepository(MiMutedNote),
inject: [DI.db],
};
const $channelsRepository: Provider = { const $channelsRepository: Provider = {
provide: DI.channelsRepository, provide: DI.channelsRepository,
useFactory: (db: DataSource) => db.getRepository(MiChannel), useFactory: (db: DataSource) => db.getRepository(MiChannel),
@ -427,7 +421,7 @@ const $noteEditRepository: Provider = {
$userPublickeysRepository, $userPublickeysRepository,
$userListsRepository, $userListsRepository,
$userListFavoritesRepository, $userListFavoritesRepository,
$userListJoiningsRepository, $userListMembershipsRepository,
$userNotePiningsRepository, $userNotePiningsRepository,
$userIpsRepository, $userIpsRepository,
$usedUsernamesRepository, $usedUsernamesRepository,
@ -460,7 +454,6 @@ const $noteEditRepository: Provider = {
$promoNotesRepository, $promoNotesRepository,
$promoReadsRepository, $promoReadsRepository,
$relaysRepository, $relaysRepository,
$mutedNotesRepository,
$channelsRepository, $channelsRepository,
$channelFollowingsRepository, $channelFollowingsRepository,
$channelFavoritesRepository, $channelFavoritesRepository,
@ -495,7 +488,7 @@ const $noteEditRepository: Provider = {
$userPublickeysRepository, $userPublickeysRepository,
$userListsRepository, $userListsRepository,
$userListFavoritesRepository, $userListFavoritesRepository,
$userListJoiningsRepository, $userListMembershipsRepository,
$userNotePiningsRepository, $userNotePiningsRepository,
$userIpsRepository, $userIpsRepository,
$usedUsernamesRepository, $usedUsernamesRepository,
@ -528,7 +521,6 @@ const $noteEditRepository: Provider = {
$promoNotesRepository, $promoNotesRepository,
$promoReadsRepository, $promoReadsRepository,
$relaysRepository, $relaysRepository,
$mutedNotesRepository,
$channelsRepository, $channelsRepository,
$channelFollowingsRepository, $channelFollowingsRepository,
$channelFavoritesRepository, $channelFavoritesRepository,

View file

@ -216,6 +216,11 @@ export class MiUser {
}) })
public isExplorable: boolean; public isExplorable: boolean;
@Column('boolean', {
default: false,
})
public isHibernated: boolean;
// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ // アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
@Column('boolean', { @Column('boolean', {
default: false, default: false,

View file

@ -8,14 +8,14 @@ import { id } from './util/id.js';
import { MiUser } from './User.js'; import { MiUser } from './User.js';
import { MiUserList } from './UserList.js'; import { MiUserList } from './UserList.js';
@Entity('user_list_joining') @Entity('user_list_membership')
@Index(['userId', 'userListId'], { unique: true }) @Index(['userId', 'userListId'], { unique: true })
export class MiUserListJoining { export class MiUserListMembership {
@PrimaryColumn(id()) @PrimaryColumn(id())
public id: string; public id: string;
@Column('timestamp with time zone', { @Column('timestamp with time zone', {
comment: 'The created date of the UserListJoining.', comment: 'The created date of the UserListMembership.',
}) })
public createdAt: Date; public createdAt: Date;
@ -44,4 +44,17 @@ export class MiUserListJoining {
}) })
@JoinColumn() @JoinColumn()
public userList: MiUserList | null; public userList: MiUserList | null;
// タイムラインにその人のリプライまで含めるかどうか
@Column('boolean', {
default: false,
})
public withReplies: boolean;
//#region Denormalized fields
@Column({
...id(),
})
public userListUserId: MiUser['id'];
//#endregion
} }

View file

@ -28,7 +28,6 @@ import { MiHashtag } from '@/models/Hashtag.js';
import { MiInstance } from '@/models/Instance.js'; import { MiInstance } from '@/models/Instance.js';
import { MiMeta } from '@/models/Meta.js'; import { MiMeta } from '@/models/Meta.js';
import { MiModerationLog } from '@/models/ModerationLog.js'; import { MiModerationLog } from '@/models/ModerationLog.js';
import { MiMutedNote } from '@/models/MutedNote.js';
import { MiMuting } from '@/models/Muting.js'; import { MiMuting } from '@/models/Muting.js';
import { MiRenoteMuting } from '@/models/RenoteMuting.js'; import { MiRenoteMuting } from '@/models/RenoteMuting.js';
import { MiNote } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js';
@ -53,7 +52,7 @@ import { MiUser } from '@/models/User.js';
import { MiUserIp } from '@/models/UserIp.js'; import { MiUserIp } from '@/models/UserIp.js';
import { MiUserKeypair } from '@/models/UserKeypair.js'; import { MiUserKeypair } from '@/models/UserKeypair.js';
import { MiUserList } from '@/models/UserList.js'; import { MiUserList } from '@/models/UserList.js';
import { MiUserListJoining } from '@/models/UserListJoining.js'; import { MiUserListMembership } from '@/models/UserListMembership.js';
import { MiUserNotePining } from '@/models/UserNotePining.js'; import { MiUserNotePining } from '@/models/UserNotePining.js';
import { MiUserPending } from '@/models/UserPending.js'; import { MiUserPending } from '@/models/UserPending.js';
import { MiUserProfile } from '@/models/UserProfile.js'; import { MiUserProfile } from '@/models/UserProfile.js';
@ -97,7 +96,6 @@ export {
MiInstance, MiInstance,
MiMeta, MiMeta,
MiModerationLog, MiModerationLog,
MiMutedNote,
MiMuting, MiMuting,
MiRenoteMuting, MiRenoteMuting,
MiNote, MiNote,
@ -123,7 +121,7 @@ export {
MiUserKeypair, MiUserKeypair,
MiUserList, MiUserList,
MiUserListFavorite, MiUserListFavorite,
MiUserListJoining, MiUserListMembership,
MiUserNotePining, MiUserNotePining,
MiUserPending, MiUserPending,
MiUserProfile, MiUserProfile,
@ -165,7 +163,6 @@ export type HashtagsRepository = Repository<MiHashtag>;
export type InstancesRepository = Repository<MiInstance>; export type InstancesRepository = Repository<MiInstance>;
export type MetasRepository = Repository<MiMeta>; export type MetasRepository = Repository<MiMeta>;
export type ModerationLogsRepository = Repository<MiModerationLog>; export type ModerationLogsRepository = Repository<MiModerationLog>;
export type MutedNotesRepository = Repository<MiMutedNote>;
export type MutingsRepository = Repository<MiMuting>; export type MutingsRepository = Repository<MiMuting>;
export type RenoteMutingsRepository = Repository<MiRenoteMuting>; export type RenoteMutingsRepository = Repository<MiRenoteMuting>;
export type NotesRepository = Repository<MiNote>; export type NotesRepository = Repository<MiNote>;
@ -191,7 +188,7 @@ export type UserIpsRepository = Repository<MiUserIp>;
export type UserKeypairsRepository = Repository<MiUserKeypair>; export type UserKeypairsRepository = Repository<MiUserKeypair>;
export type UserListsRepository = Repository<MiUserList>; export type UserListsRepository = Repository<MiUserList>;
export type UserListFavoritesRepository = Repository<MiUserListFavorite>; export type UserListFavoritesRepository = Repository<MiUserListFavorite>;
export type UserListJoiningsRepository = Repository<MiUserListJoining>; export type UserListMembershipsRepository = Repository<MiUserListMembership>;
export type UserNotePiningsRepository = Repository<MiUserNotePining>; export type UserNotePiningsRepository = Repository<MiUserNotePining>;
export type UserPendingsRepository = Repository<MiUserPending>; export type UserPendingsRepository = Repository<MiUserPending>;
export type UserProfilesRepository = Repository<MiUserProfile>; export type UserProfilesRepository = Repository<MiUserProfile>;

View file

@ -17,11 +17,6 @@ export const packedNoteSchema = {
optional: false, nullable: false, optional: false, nullable: false,
format: 'date-time', format: 'date-time',
}, },
updatedAt: {
type: 'string',
optional: true, nullable: true,
format: 'date-time',
},
deletedAt: { deletedAt: {
type: 'string', type: 'string',
optional: true, nullable: true, optional: true, nullable: true,

View file

@ -296,6 +296,10 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'string', type: 'string',
nullable: false, optional: true, nullable: false, optional: true,
}, },
withReplies: {
type: 'boolean',
nullable: false, optional: true,
},
//#endregion //#endregion
}, },
} as const; } as const;

View file

@ -36,7 +36,6 @@ import { MiHashtag } from '@/models/Hashtag.js';
import { MiInstance } from '@/models/Instance.js'; import { MiInstance } from '@/models/Instance.js';
import { MiMeta } from '@/models/Meta.js'; import { MiMeta } from '@/models/Meta.js';
import { MiModerationLog } from '@/models/ModerationLog.js'; import { MiModerationLog } from '@/models/ModerationLog.js';
import { MiMutedNote } from '@/models/MutedNote.js';
import { MiMuting } from '@/models/Muting.js'; import { MiMuting } from '@/models/Muting.js';
import { MiRenoteMuting } from '@/models/RenoteMuting.js'; import { MiRenoteMuting } from '@/models/RenoteMuting.js';
import { MiNote } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js';
@ -62,7 +61,7 @@ import { MiUserIp } from '@/models/UserIp.js';
import { MiUserKeypair } from '@/models/UserKeypair.js'; import { MiUserKeypair } from '@/models/UserKeypair.js';
import { MiUserList } from '@/models/UserList.js'; import { MiUserList } from '@/models/UserList.js';
import { MiUserListFavorite } from '@/models/UserListFavorite.js'; import { MiUserListFavorite } from '@/models/UserListFavorite.js';
import { MiUserListJoining } from '@/models/UserListJoining.js'; import { MiUserListMembership } from '@/models/UserListMembership.js';
import { MiUserNotePining } from '@/models/UserNotePining.js'; import { MiUserNotePining } from '@/models/UserNotePining.js';
import { MiUserPending } from '@/models/UserPending.js'; import { MiUserPending } from '@/models/UserPending.js';
import { MiUserProfile } from '@/models/UserProfile.js'; import { MiUserProfile } from '@/models/UserProfile.js';
@ -139,7 +138,7 @@ export const entities = [
MiUserPublickey, MiUserPublickey,
MiUserList, MiUserList,
MiUserListFavorite, MiUserListFavorite,
MiUserListJoining, MiUserListMembership,
MiUserNotePining, MiUserNotePining,
MiUserSecurityKey, MiUserSecurityKey,
MiUsedUsername, MiUsedUsername,
@ -175,7 +174,6 @@ export const entities = [
MiPromoNote, MiPromoNote,
MiPromoRead, MiPromoRead,
MiRelay, MiRelay,
MiMutedNote,
MiChannel, MiChannel,
MiChannelFollowing, MiChannelFollowing,
MiChannelFavorite, MiChannelFavorite,

View file

@ -6,7 +6,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { In, LessThan } from 'typeorm'; import { In, LessThan } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { AntennasRepository, MutedNotesRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/_.js'; import type { AntennasRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/_.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
@ -25,9 +25,6 @@ export class CleanProcessorService {
@Inject(DI.userIpsRepository) @Inject(DI.userIpsRepository)
private userIpsRepository: UserIpsRepository, private userIpsRepository: UserIpsRepository,
@Inject(DI.mutedNotesRepository)
private mutedNotesRepository: MutedNotesRepository,
@Inject(DI.antennasRepository) @Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository, private antennasRepository: AntennasRepository,
@ -48,16 +45,6 @@ export class CleanProcessorService {
createdAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90))), createdAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90))),
}); });
this.mutedNotesRepository.delete({
id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))),
reason: 'word',
});
this.mutedNotesRepository.delete({
id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))),
reason: 'word',
});
// 使われてないアンテナを停止 // 使われてないアンテナを停止
if (this.config.deactivateAntennaThreshold > 0) { if (this.config.deactivateAntennaThreshold > 0) {
this.antennasRepository.update({ this.antennasRepository.update({

View file

@ -8,7 +8,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { format as DateFormat } from 'date-fns'; import { format as DateFormat } from 'date-fns';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { AntennasRepository, UsersRepository, UserListJoiningsRepository, MiUser } from '@/models/_.js'; import type { AntennasRepository, UsersRepository, UserListMembershipsRepository, MiUser } from '@/models/_.js';
import Logger from '@/logger.js'; import Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -29,8 +29,8 @@ export class ExportAntennasProcessorService {
@Inject(DI.antennasRepository) @Inject(DI.antennasRepository)
private antennsRepository: AntennasRepository, private antennsRepository: AntennasRepository,
@Inject(DI.userListJoiningsRepository) @Inject(DI.userListMembershipsRepository)
private userListJoiningsRepository: UserListJoiningsRepository, private userListMembershipsRepository: UserListMembershipsRepository,
private driveService: DriveService, private driveService: DriveService,
private utilityService: UtilityService, private utilityService: UtilityService,
@ -65,9 +65,9 @@ export class ExportAntennasProcessorService {
for (const [index, antenna] of antennas.entries()) { for (const [index, antenna] of antennas.entries()) {
let users: MiUser[] | undefined; let users: MiUser[] | undefined;
if (antenna.userListId !== null) { if (antenna.userListId !== null) {
const joinings = await this.userListJoiningsRepository.findBy({ userListId: antenna.userListId }); const memberships = await this.userListMembershipsRepository.findBy({ userListId: antenna.userListId });
users = await this.usersRepository.findBy({ users = await this.usersRepository.findBy({
id: In(joinings.map(j => j.userId)), id: In(memberships.map(j => j.userId)),
}); });
} }
write(JSON.stringify({ write(JSON.stringify({

View file

@ -8,7 +8,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { format as dateFormat } from 'date-fns'; import { format as dateFormat } from 'date-fns';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { UserListJoiningsRepository, UserListsRepository, UsersRepository } from '@/models/_.js'; import type { UserListMembershipsRepository, UserListsRepository, UsersRepository } from '@/models/_.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js'; import { createTemp } from '@/misc/create-temp.js';
@ -29,8 +29,8 @@ export class ExportUserListsProcessorService {
@Inject(DI.userListsRepository) @Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository, private userListsRepository: UserListsRepository,
@Inject(DI.userListJoiningsRepository) @Inject(DI.userListMembershipsRepository)
private userListJoiningsRepository: UserListJoiningsRepository, private userListMembershipsRepository: UserListMembershipsRepository,
private utilityService: UtilityService, private utilityService: UtilityService,
private driveService: DriveService, private driveService: DriveService,
@ -61,9 +61,9 @@ export class ExportUserListsProcessorService {
const stream = fs.createWriteStream(path, { flags: 'a' }); const stream = fs.createWriteStream(path, { flags: 'a' });
for (const list of lists) { for (const list of lists) {
const joinings = await this.userListJoiningsRepository.findBy({ userListId: list.id }); const memberships = await this.userListMembershipsRepository.findBy({ userListId: list.id });
const users = await this.usersRepository.findBy({ const users = await this.usersRepository.findBy({
id: In(joinings.map(j => j.userId)), id: In(memberships.map(j => j.userId)),
}); });
for (const u of users) { for (const u of users) {

View file

@ -6,7 +6,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { UsersRepository, DriveFilesRepository, UserListJoiningsRepository, UserListsRepository } from '@/models/_.js'; import type { UsersRepository, DriveFilesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import * as Acct from '@/misc/acct.js'; import * as Acct from '@/misc/acct.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
@ -33,8 +33,8 @@ export class ImportUserListsProcessorService {
@Inject(DI.userListsRepository) @Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository, private userListsRepository: UserListsRepository,
@Inject(DI.userListJoiningsRepository) @Inject(DI.userListMembershipsRepository)
private userListJoiningsRepository: UserListJoiningsRepository, private userListMembershipsRepository: UserListMembershipsRepository,
private utilityService: UtilityService, private utilityService: UtilityService,
private idService: IdService, private idService: IdService,
@ -99,7 +99,7 @@ export class ImportUserListsProcessorService {
target = await this.remoteUserResolveService.resolveUser(username, host); target = await this.remoteUserResolveService.resolveUser(username, host);
} }
if (await this.userListJoiningsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue; if (await this.userListMembershipsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
this.userListService.addMember(target, list!, user); this.userListService.addMember(target, list!, user);
} catch (e) { } catch (e) {

View file

@ -379,7 +379,8 @@ export class ActivityPubServerService {
if (page) { if (page) {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId) const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId)
.andWhere('note.userId = :userId', { userId: user.id }) .andWhere('note.userId = :userId', { userId: user.id })
.andWhere(new Brackets(qb => { qb .andWhere(new Brackets(qb => {
qb
.where('note.visibility = \'public\'') .where('note.visibility = \'public\'')
.orWhere('note.visibility = \'home\''); .orWhere('note.visibility = \'home\'');
})) }))

View file

@ -102,6 +102,8 @@ export class NodeinfoServerService {
}, },
langs: meta.langs, langs: meta.langs,
tosUrl: meta.termsOfServiceUrl, tosUrl: meta.termsOfServiceUrl,
privacyPolicyUrl: meta.privacyPolicyUrl,
impressumUrl: meta.impressumUrl,
repositoryUrl: meta.repositoryUrl, repositoryUrl: meta.repositoryUrl,
feedbackUrl: meta.feedbackUrl, feedbackUrl: meta.feedbackUrl,
disableRegistration: meta.disableRegistration, disableRegistration: meta.disableRegistration,
@ -133,7 +135,11 @@ export class NodeinfoServerService {
.type( .type(
'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.1#"', 'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.1#"',
) )
.header('Cache-Control', 'public, max-age=600'); .header('Cache-Control', 'public, max-age=600')
.header('Access-Control-Allow-Headers', 'Accept')
.header('Access-Control-Allow-Methods', 'GET, OPTIONS')
.header('Access-Control-Allow-Origin', '*')
.header('Access-Control-Expose-Headers', 'Vary');
return { version: '2.1', ...base }; return { version: '2.1', ...base };
}); });
@ -146,7 +152,11 @@ export class NodeinfoServerService {
.type( .type(
'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"', 'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"',
) )
.header('Cache-Control', 'public, max-age=600'); .header('Cache-Control', 'public, max-age=600')
.header('Access-Control-Allow-Headers', 'Accept')
.header('Access-Control-Allow-Methods', 'GET, OPTIONS')
.header('Access-Control-Allow-Origin', '*')
.header('Access-Control-Expose-Headers', 'Vary');
return { version: '2.0', ...base }; return { version: '2.0', ...base };
}); });

Some files were not shown because too many files have changed in this diff Show more