Merge branch 'develop' into feat/scylladb
This commit is contained in:
commit
57df63c308
82 changed files with 379 additions and 1798 deletions
|
@ -9,7 +9,7 @@ By submitting this merge request, you agree to follow our [Contribution Guidelin
|
|||
- [ ] I have made sure to run `pnpm run format` before submitting this pull request
|
||||
|
||||
If this merge request makes changes to the Firefish API, please update `docs/api-change.md`
|
||||
- [ ] I updated the documentation
|
||||
- [ ] I updated the document / This merge request doesn't include API changes
|
||||
|
||||
<!-- Uncomment if your merge request has multiple authors -->
|
||||
<!-- Co-authored-by: Name <email@email.com> -->
|
||||
|
|
|
@ -122,7 +122,7 @@ db:
|
|||
port: 5432
|
||||
{{- else }}
|
||||
host: {{ .Values.postgresql.postgresqlHostname }}
|
||||
port: {{ .Values.postgresql.postgresqlPort | default "5432" | quote }}
|
||||
port: {{ .Values.postgresql.postgresqlPort | default 5432 }}
|
||||
{{- end }}
|
||||
|
||||
# Database name
|
||||
|
|
|
@ -4,6 +4,24 @@ Breaking changes are indicated by the :warning: icon.
|
|||
|
||||
## v1.0.5 (unreleased)
|
||||
|
||||
### dev18
|
||||
|
||||
- :warning: response of `meta` no longer includes the following:
|
||||
- `enableTwitterIntegration`
|
||||
- `enableGithubIntegration`
|
||||
- `enableDiscordIntegration`
|
||||
- :warning: parameter of `admin/update-meta` and response of `admin/meta` no longer include the following:
|
||||
- `enableTwitterIntegration`
|
||||
- `enableGithubIntegration`
|
||||
- `enableDiscordIntegration`
|
||||
- `twitterConsumerKey`
|
||||
- `twitterConsumerSecret`
|
||||
- `githubClientId`
|
||||
- `githubClientSecret`
|
||||
- `discordClientId`
|
||||
- `discordClientSecret`
|
||||
- :warning: response of `admin/show-user` no longer includes `integrations`.
|
||||
|
||||
### dev17
|
||||
|
||||
- Added `lang` parameter to `notes/create` and `notes/edit`.
|
||||
|
|
|
@ -312,9 +312,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "الصفحات"
|
||||
integration: "التكامل"
|
||||
connectService: "اتصل"
|
||||
disconnectService: "اقطع الاتصال"
|
||||
enableLocalTimeline: "تفعيل الخيط المحلي"
|
||||
enableGlobalTimeline: "تفعيل الخيط الزمني الشامل"
|
||||
disablingTimelinesInfo: "سيتمكن المديرون والمشرفون من الوصول إلى كل الخيوط الزمنية حتى وإن لم تفعّل."
|
||||
|
|
|
@ -316,9 +316,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "পৃষ্ঠা"
|
||||
integration: "ইন্টিগ্রেশন"
|
||||
connectService: "সংযুক্ত করুন"
|
||||
disconnectService: "সংযোগ বিচ্ছিন্ন করুন"
|
||||
enableLocalTimeline: "স্থানীয় টাইমলাইন চালু করুন"
|
||||
enableGlobalTimeline: "গ্লোবাল টাইমলাইন চালু করুন"
|
||||
disablingTimelinesInfo: "আপনি এই টাইমলাইনগুলি বন্ধ করলেও প্রশাসক এবং মডারেটররা এই টাইমলাইনগুলি ব্যাবহার করতে পারবে"
|
||||
|
|
|
@ -944,7 +944,6 @@ dayX: '{day}'
|
|||
tosUrl: URL de les Condicions d'ús
|
||||
thisYear: Any
|
||||
thisMonth: Mes
|
||||
integration: Integracions
|
||||
driveCapacityPerRemoteAccount: Capacitat del Disc per usuari remot
|
||||
inMb: En megabytes
|
||||
iconUrl: Adreça URL de la icona
|
||||
|
@ -1036,8 +1035,6 @@ accept: Accepta
|
|||
reject: Rebutja
|
||||
yearX: '{year}'
|
||||
pages: Pàgines
|
||||
disconnectService: Desconnectar
|
||||
connectService: Connectar
|
||||
enableLocalTimeline: Activa la línea de temps local
|
||||
enableRecommendedTimeline: Activa la línea de temps de recomanacions
|
||||
pinnedClipId: ID del clip que vols fixar
|
||||
|
|
|
@ -310,9 +310,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Stránky"
|
||||
integration: "Integrace"
|
||||
connectService: "Připojit"
|
||||
disconnectService: "Odpojit"
|
||||
enableLocalTimeline: "Povolit lokální čas"
|
||||
enableGlobalTimeline: "Povolit globální čas"
|
||||
registration: "Registrace"
|
||||
|
|
|
@ -350,9 +350,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Nutzer-Seiten"
|
||||
integration: "Integration"
|
||||
connectService: "Verbinden"
|
||||
disconnectService: "Trennen"
|
||||
enableLocalTimeline: "Local-Timeline aktivieren"
|
||||
enableGlobalTimeline: "Global-Timeline aktivieren"
|
||||
disablingTimelinesInfo: "Administratoren und Moderatoren haben immer Zugriff auf alle
|
||||
|
@ -529,12 +526,12 @@ objectStorageBaseUrl: "Basis-URL"
|
|||
objectStorageBaseUrlDesc: "Die als Referenz verwendete URL. Verwendest du einen CDN
|
||||
oder Proxy, gib dessen URL an. \nFür S3 verwende 'https://<bucket>.s3.amazonaws.com'.
|
||||
Für GCS o.ä. verwende 'https://storage.googleapis.com/<bucket>'."
|
||||
objectStorageBucket: "Eimer"
|
||||
objectStorageBucket: "Bucket"
|
||||
objectStorageBucketDesc: "Bitte gib den Namen des Buckets an, der bei deinem Anbieter
|
||||
verwendet wird."
|
||||
objectStoragePrefix: "Prefix"
|
||||
objectStoragePrefixDesc: "Dateien werden in Ordnern unter diesem Prefix gespeichert."
|
||||
objectStorageEndpoint: "Limit"
|
||||
objectStorageEndpoint: "Endpunkt"
|
||||
objectStorageEndpointDesc: "Im Falle von S3 leerlassen, für andere Anbieter den relevanten
|
||||
Endpoint im Format „<host>“ oder „<host>:<port>“ angeben."
|
||||
objectStorageRegion: "Region"
|
||||
|
@ -1037,6 +1034,7 @@ _accountDelete:
|
|||
_ad:
|
||||
back: "Zurück"
|
||||
reduceFrequencyOfThisAd: "Diese Werbeanzeige weniger anzeigen"
|
||||
adsBy: Community-Banner von {by}
|
||||
_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."
|
||||
|
@ -1243,6 +1241,7 @@ _wordMute:
|
|||
soft: "Leicht"
|
||||
hard: "Schwer"
|
||||
mutedNotes: "Stummgeschaltete Beiträge"
|
||||
muteLangs: Stummgeschaltete Sprachen
|
||||
_instanceMute:
|
||||
instanceMuteDescription: "Schaltet alle Beiträge/Boosts stumm, die von den gelisteten
|
||||
Servern stammen, inklusive Antworten von Nutzern an einen Nutzer eines stummgeschalteten
|
||||
|
@ -2215,3 +2214,5 @@ indexable: Indexierbar
|
|||
languageForTranslation: Übersetzungssprache veröffentlichen
|
||||
openServerInfo: Anzeigen von Serverinformationen durch Anklicken des Server-Tickers
|
||||
in einem Beitrag
|
||||
vibrate: Vibrationen abspielen
|
||||
clickToShowPatterns: Klicken um Modul-Muster anzuzeigen
|
||||
|
|
|
@ -215,8 +215,6 @@ thisMonth: "Μήνας"
|
|||
today: "Σήμερα"
|
||||
dayX: "{day}"
|
||||
pages: "Σελίδες"
|
||||
connectService: "Σύνδεση"
|
||||
disconnectService: "Αποσύνδεση"
|
||||
registration: "Εγγραφή"
|
||||
pinnedPages: "Καρφιτσωμένες Σελίδες"
|
||||
pinnedNotes: "Καρφιτσωμένες δημοσιεύσεις"
|
||||
|
@ -730,7 +728,6 @@ lightThemes: Φωτεινά θέματα
|
|||
darkThemes: Σκοτεινά θέματα
|
||||
inputNewFolderName: Πληκτρολογήστε ένα νέο όνομα φακέλου
|
||||
hasChildFilesOrFolders: Εφόσον αυτός ο φάκελος δεν είναι άδειος, δεν μπορεί να διαγραφεί.
|
||||
integration: Ενσωματώσεις
|
||||
enableRecommendedTimeline: Ενεργοποίηση χρονολογίου προτεινόμενων
|
||||
driveCapacityPerLocalAccount: Μέγεθος Αποθηκευτικού Χώρου ανά τοπικό μέλος
|
||||
driveCapacityPerRemoteAccount: Μέγεθος Αποθηκευτικού Χώρου ανά απομακρυσμένο μέλος
|
||||
|
|
|
@ -58,6 +58,7 @@ sendMessage: "Send a message"
|
|||
copyUsername: "Copy username"
|
||||
searchUser: "Search for a user"
|
||||
reply: "Reply"
|
||||
replies: "Replies"
|
||||
jumpToPrevious: "Jump to previous"
|
||||
loadMore: "Load more"
|
||||
showMore: "Show more"
|
||||
|
@ -112,18 +113,21 @@ unfollow: "Unfollow"
|
|||
followRequestPending: "Follow request pending"
|
||||
enterEmoji: "Enter an emoji"
|
||||
renote: "Boost"
|
||||
renotes: "Boosts"
|
||||
unrenote: "Take back boost"
|
||||
renoted: "Boosted."
|
||||
cantRenote: "This post can't be boosted."
|
||||
cantReRenote: "A boost can't be boosted."
|
||||
quote: "Quote"
|
||||
quotes: "Quotes"
|
||||
pinnedNote: "Pinned post"
|
||||
pinned: "Pin to profile"
|
||||
you: "You"
|
||||
clickToShow: "Click to show"
|
||||
sensitive: "NSFW"
|
||||
add: "Add"
|
||||
reaction: "Reactions"
|
||||
reaction: "Reaction"
|
||||
reactions: "Reactions"
|
||||
removeReaction: "Remove your reaction"
|
||||
enableEmojiReactions: "Enable emoji reactions"
|
||||
showEmojisInReactionNotifications: "Show emojis in reaction notifications"
|
||||
|
@ -371,9 +375,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Pages"
|
||||
integration: "Integrations"
|
||||
connectService: "Connect"
|
||||
disconnectService: "Disconnect"
|
||||
enableLocalTimeline: "Enable local timeline"
|
||||
enableGlobalTimeline: "Enable global timeline"
|
||||
enableRecommendedTimeline: "Enable recommended timeline"
|
||||
|
@ -739,6 +740,7 @@ system: "System"
|
|||
switchUi: "Layout"
|
||||
desktop: "Desktop"
|
||||
clip: "Clip"
|
||||
clips: "Clips"
|
||||
createNew: "Create new"
|
||||
optional: "Optional"
|
||||
createNewClip: "Create new clip"
|
||||
|
@ -781,7 +783,6 @@ pageLikesCount: "Number of liked Pages"
|
|||
pageLikedCount: "Number of received Page likes"
|
||||
contact: "Contact"
|
||||
useSystemFont: "Use the system's default font"
|
||||
clips: "Clips"
|
||||
clipsDesc: "Clips are like share-able categorized bookmarks. You can create clips
|
||||
from the menu of individual posts."
|
||||
experimentalFeatures: "Experimental features"
|
||||
|
|
|
@ -340,9 +340,6 @@ dayX: "Día {day}"
|
|||
monthX: "Mes {month}"
|
||||
yearX: "Año {year}"
|
||||
pages: "Páginas"
|
||||
integration: "Integraciones"
|
||||
connectService: "Conectar"
|
||||
disconnectService: "Desconectar"
|
||||
enableLocalTimeline: "Habilitar linea de tiempo local"
|
||||
enableGlobalTimeline: "Habilitar linea de tiempo global"
|
||||
disablingTimelinesInfo: "Aunque se desactiven estas lineas de tiempo, por conveniencia
|
||||
|
|
|
@ -339,8 +339,6 @@ instanceName: Instanssin nimi
|
|||
thisMonth: Kuukausi
|
||||
today: Tänään
|
||||
monthX: '{month}'
|
||||
connectService: Yhdistä
|
||||
disconnectService: Katkaise yhteys
|
||||
enableLocalTimeline: Ota käyttöön paikallinen aikajana
|
||||
enableGlobalTimeline: Ota käyttöön globaali aikajana
|
||||
enableRecommendedTimeline: Ota käyttöön suositellut -aikajana
|
||||
|
@ -385,7 +383,6 @@ disablingTimelinesInfo: Järjestelmänvalvojilla ja moderaattoreilla on aina pä
|
|||
dayX: '{day}'
|
||||
yearX: '{year}'
|
||||
pages: Sivut
|
||||
integration: Integraatiot
|
||||
instanceDescription: Instanssin kuvaus
|
||||
invite: Kutsu
|
||||
iconUrl: Ikoni URL-linkki
|
||||
|
|
|
@ -341,9 +341,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Pages"
|
||||
integration: "Intégrations"
|
||||
connectService: "Connexion"
|
||||
disconnectService: "Déconnexion"
|
||||
enableLocalTimeline: "Activer le fil local"
|
||||
enableGlobalTimeline: "Activer le fil global"
|
||||
disablingTimelinesInfo: "Même si vous désactivez ces fils, les administrateur·rice·s
|
||||
|
|
|
@ -342,9 +342,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Halaman"
|
||||
integration: "Integrasi"
|
||||
connectService: "Sambungkan"
|
||||
disconnectService: "Putuskan"
|
||||
enableLocalTimeline: "Nyalakan linimasa lokal"
|
||||
enableGlobalTimeline: "Nyalakan linimasa global"
|
||||
disablingTimelinesInfo: "Admin dan Moderator akan selalu memiliki akses ke semua linimasa
|
||||
|
|
|
@ -331,9 +331,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Pagine"
|
||||
integration: "Integrazioni"
|
||||
connectService: "Connetti"
|
||||
disconnectService: "Disconnetti"
|
||||
enableLocalTimeline: "Abilita timeline locale"
|
||||
enableGlobalTimeline: "Abilita timeline federata"
|
||||
disablingTimelinesInfo: "Anche se disabiliti queste timeline, gli amministratori e
|
||||
|
|
|
@ -52,6 +52,7 @@ sendMessage: "メッセージを送信"
|
|||
copyUsername: "ユーザー名をコピー"
|
||||
searchUser: "ユーザーを検索"
|
||||
reply: "返信"
|
||||
replies: "返信"
|
||||
loadMore: "もっと読み込む"
|
||||
showMore: "もっと見る"
|
||||
showLess: "閉じる"
|
||||
|
@ -97,11 +98,13 @@ unfollow: "フォロー解除"
|
|||
followRequestPending: "フォロー許可待ち"
|
||||
enterEmoji: "絵文字を入力"
|
||||
renote: "ブースト"
|
||||
renotes: "ブースト"
|
||||
unrenote: "ブースト解除"
|
||||
renoted: "ブーストしました。"
|
||||
cantRenote: "この投稿はブーストできません。"
|
||||
cantReRenote: "ブーストをブーストすることはできません。"
|
||||
quote: "引用"
|
||||
quotes: "引用"
|
||||
pinnedNote: "ピン留めされた投稿"
|
||||
pinned: "ピン留め"
|
||||
you: "あなた"
|
||||
|
@ -109,6 +112,7 @@ clickToShow: "クリックして表示"
|
|||
sensitive: "閲覧注意"
|
||||
add: "追加"
|
||||
reaction: "リアクション"
|
||||
reactions: "リアクション"
|
||||
enableEmojiReactions: "絵文字リアクションを有効にする"
|
||||
showEmojisInReactionNotifications: "自分の投稿に対するリアクションの通知で絵文字を表示する"
|
||||
reactionSetting: "ピッカーに表示するリアクション"
|
||||
|
@ -334,9 +338,6 @@ dayX: "{day}日"
|
|||
monthX: "{month}月"
|
||||
yearX: "{year}年"
|
||||
pages: "ページ"
|
||||
integration: "連携"
|
||||
connectService: "接続する"
|
||||
disconnectService: "切断する"
|
||||
enableLocalTimeline: "ローカルタイムラインを有効にする"
|
||||
enableGlobalTimeline: "グローバルタイムラインを有効にする"
|
||||
enableRecommendedTimeline: "おすすめタイムラインを有効にする"
|
||||
|
@ -1504,7 +1505,7 @@ _profile:
|
|||
youCanIncludeHashtags: "ハッシュタグを含められます。"
|
||||
metadata: "追加情報"
|
||||
metadataEdit: "追加情報を編集"
|
||||
metadataDescription: "プロフィールに追加情報を表示できます。プロフィールにリンクしたウェブサイトやSNSアカウントに<link {rel}>タグ(または{a}タグ)を追加すると、そのリンクを本人認証できます。"
|
||||
metadataDescription: "プロフィールに追加情報を表示できます。{rel}属性をつけた{a}タグや{l}タグを含むページをリンクすることで、リンクの本人確認もできます!"
|
||||
metadataLabel: "ラベル"
|
||||
metadataContent: "内容"
|
||||
changeAvatar: "アバター画像を変更"
|
||||
|
|
|
@ -317,9 +317,6 @@ dayX: "{day}日"
|
|||
monthX: "{month}月"
|
||||
yearX: "{year}年"
|
||||
pages: "ページ"
|
||||
integration: "連携"
|
||||
connectService: "つなげるで"
|
||||
disconnectService: "切るで"
|
||||
enableLocalTimeline: "ローカルタイムラインを使えるようにする"
|
||||
enableGlobalTimeline: "グローバルタイムラインを使えるようにする"
|
||||
disablingTimelinesInfo: "ここらへんのタイムラインを使えんようにしてしもても、管理者とモデレーターは使えるままになってるで、そうやなかったら不便やからな。"
|
||||
|
|
|
@ -36,7 +36,6 @@ selectList: "Fren tabdart"
|
|||
youHaveNoLists: "Ulac ɣur-k·m ula d yiwet n tabdart"
|
||||
security: "Taɣellist"
|
||||
remove: "Kkes"
|
||||
connectService: "Qqen"
|
||||
userList: "Tibdarin"
|
||||
securityKey: "Tasarutt n tɣellist"
|
||||
securityKeyName: "Isem n tsarutt"
|
||||
|
|
|
@ -323,9 +323,6 @@ dayX: "{day}일"
|
|||
monthX: "{month}월"
|
||||
yearX: "{year}년"
|
||||
pages: "페이지"
|
||||
integration: "연동"
|
||||
connectService: "계정 연동"
|
||||
disconnectService: "계정 연동 해제"
|
||||
enableLocalTimeline: "로컬 타임라인 활성화"
|
||||
enableGlobalTimeline: "글로벌 타임라인 활성화"
|
||||
disablingTimelinesInfo: "특정 타임라인을 비활성화하더라도 관리자 및 모더레이터는 계속 사용할 수 있습니다."
|
||||
|
|
|
@ -483,8 +483,6 @@ accept: Accepteren
|
|||
reject: Afwijzen
|
||||
normal: Normaal
|
||||
pages: Pagina's
|
||||
integration: Integraties
|
||||
connectService: Koppelen
|
||||
monthX: '{month}'
|
||||
yearX: '{year}'
|
||||
instanceName: Servernaam
|
||||
|
@ -492,7 +490,6 @@ instanceDescription: Server omschrijving
|
|||
maintainerName: Onderhouder
|
||||
maintainerEmail: Onderhouder email
|
||||
tosUrl: Algemene Voorwaarden URL
|
||||
disconnectService: Ontkoppelen
|
||||
unread: Ongelezen
|
||||
manageGroups: Beheer groepen
|
||||
subscribePushNotification: Pushmeldingen inschakelen
|
||||
|
|
|
@ -225,8 +225,6 @@ instanceDescription: Tjenerbeskrivelse
|
|||
maintainerName: Administrator
|
||||
maintainerEmail: Administrator-epost
|
||||
monthX: '{month}'
|
||||
connectService: Koble til
|
||||
disconnectService: Koble fra
|
||||
enableLocalTimeline: Aktiver lokal tidslinje
|
||||
enableRegistration: Tillat registrering av nye brukere
|
||||
invite: Inviter
|
||||
|
@ -449,7 +447,6 @@ watch: Følg med på
|
|||
thisMonth: Måned
|
||||
today: I dag
|
||||
dayX: '{day}'
|
||||
integration: Integrasjoner
|
||||
yearX: '{year}'
|
||||
pages: Sider
|
||||
enableRecaptcha: Slå på reCAPTCHA
|
||||
|
|
|
@ -331,9 +331,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Strony"
|
||||
integration: "Integracje"
|
||||
connectService: "Połącz"
|
||||
disconnectService: "Rozłącz"
|
||||
enableLocalTimeline: "Włącz lokalną oś czasu"
|
||||
enableGlobalTimeline: "Włącz globalną oś czasu"
|
||||
disablingTimelinesInfo: "Administratorzy i moderatorzy będą zawsze mieć dostęp do
|
||||
|
|
|
@ -339,9 +339,6 @@ dayX: " Dia {day}"
|
|||
monthX: "mês de {month}"
|
||||
yearX: "Ano {year}"
|
||||
pages: "Páginas"
|
||||
integration: "Integração"
|
||||
connectService: "Conectar"
|
||||
disconnectService: "Desconectar"
|
||||
enableLocalTimeline: "Ativar linha do tempo local"
|
||||
enableGlobalTimeline: "Ativar linha do tempo global"
|
||||
disablingTimelinesInfo: "Se você desabilitar essas linhas do tempo, administradores
|
||||
|
|
|
@ -316,9 +316,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Pagini"
|
||||
integration: "Integrare"
|
||||
connectService: "Conectează"
|
||||
disconnectService: "Deconectează"
|
||||
enableLocalTimeline: "Activează cronologia locală"
|
||||
enableGlobalTimeline: "Activeaza cronologia globală"
|
||||
disablingTimelinesInfo: "Administratorii și Moderatorii vor avea mereu access la toate cronologiile, chiar dacă nu sunt activate."
|
||||
|
|
|
@ -335,9 +335,6 @@ dayX: "{day} день"
|
|||
monthX: "{month} месяц"
|
||||
yearX: "{year} год"
|
||||
pages: "Страницы"
|
||||
integration: "Интеграции"
|
||||
connectService: "Подключиться"
|
||||
disconnectService: "Отключиться"
|
||||
enableLocalTimeline: "Включить локальную ленту"
|
||||
enableGlobalTimeline: "Включить глобальную ленту"
|
||||
disablingTimelinesInfo: "У администраторов и модераторов есть доступ ко всем лентам,
|
||||
|
|
|
@ -317,9 +317,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Stránky"
|
||||
integration: "Integrácia"
|
||||
connectService: "Pripojiť"
|
||||
disconnectService: "Odpojiť"
|
||||
enableLocalTimeline: "Povoliť lokálnu časovú os"
|
||||
enableGlobalTimeline: "Povoliť globálnu časovú os"
|
||||
disablingTimelinesInfo: "Administrátori a moderátori majú vždy prístup ku všetkým časovým osiam, aj keď sú vypnuté."
|
||||
|
|
|
@ -365,8 +365,6 @@ today: Idag
|
|||
dayX: '{day}'
|
||||
monthX: '{month}'
|
||||
yearX: '{year}'
|
||||
connectService: Anslut
|
||||
disconnectService: Bortkoppla
|
||||
enableLocalTimeline: Anslut till lokal tidslinje
|
||||
invite: Bjud in
|
||||
driveCapacityPerLocalAccount: Enhetens kapacitet per lokal användare
|
||||
|
@ -779,7 +777,6 @@ serverLogs: Serverloggar
|
|||
deleteAll: Radera alla
|
||||
removeAllFollowing: Sluta följa alla följda användare
|
||||
medium: Mellan
|
||||
integration: Integreringar
|
||||
xl: XL
|
||||
desktop: Skrivbord
|
||||
createNew: Skapa nya
|
||||
|
|
|
@ -327,9 +327,6 @@ dayX: "{วัน}"
|
|||
monthX: "{เดือน}"
|
||||
yearX: "{ปี}"
|
||||
pages: "หน้า"
|
||||
integration: "รวบรวม"
|
||||
connectService: "เชื่อมต่อ"
|
||||
disconnectService: "ตัดการเชื่อมต่อ"
|
||||
enableLocalTimeline: "เปิดใช้งานไทม์ไลน์ในพื้นที่"
|
||||
enableGlobalTimeline: "เปิดใช้งานไทม์ไลน์ทั่วโลก"
|
||||
disablingTimelinesInfo: "ผู้ดูแลระบบและผู้ควบคุมจะสามารถเข้าถึงไทม์ไลน์ทั้งหมด ถึงแม้ว่าจะไม่ได้เปิดใช้งานก็ตาม"
|
||||
|
|
|
@ -760,7 +760,6 @@ banner: Banner
|
|||
nsfw: NSFW
|
||||
doNothing: Görmezden Gel
|
||||
watch: İzle
|
||||
connectService: Bağlan
|
||||
registration: Kayıt
|
||||
hcaptcha: hCaptcha
|
||||
pinnedNotes: Sabitlenmiş gönderiler
|
||||
|
@ -885,7 +884,6 @@ deleteFolder: Bu klasörü sil
|
|||
addFile: Dosya ekle
|
||||
dayX: '{day}'
|
||||
enableLocalTimeline: Yerel zaman çizgisini aktif et
|
||||
disconnectService: Bağlantıyı kes
|
||||
enableGlobalTimeline: Global zaman çizgisini aktif et
|
||||
enableRegistration: Yeni kullanıcı kaydını aktif et
|
||||
invite: Davet et
|
||||
|
@ -1206,7 +1204,6 @@ location: Konum
|
|||
registeredDate: Katılım tarihi
|
||||
yearX: '{year}'
|
||||
pages: Sayfalar
|
||||
integration: Entegrasyonlar
|
||||
antennasDesc: "Antenler, belirlediğiniz kriterlere uyan yeni gönderiler görüntüler!\n
|
||||
Zaman çizelgeleri sayfasından erişilebilirler."
|
||||
notesAndReplies: Gönderiler ve yanıtlar
|
||||
|
|
|
@ -336,9 +336,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Сторінки"
|
||||
integration: "Інтеграції"
|
||||
connectService: "Під’єднати"
|
||||
disconnectService: "Відключитися"
|
||||
enableLocalTimeline: "Увімкнути локальну стрічку"
|
||||
enableGlobalTimeline: "Увімкнути глобальну стрічку"
|
||||
disablingTimelinesInfo: "Адміністратори та модератори завжди мають доступ до всіх
|
||||
|
|
|
@ -338,9 +338,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Trang"
|
||||
integration: "Tương tác"
|
||||
connectService: "Kết nối"
|
||||
disconnectService: "Ngắt kết nối"
|
||||
enableLocalTimeline: "Bật bảng tin máy chủ"
|
||||
enableGlobalTimeline: "Bật bảng tin liên hợp"
|
||||
disablingTimelinesInfo: "Quản trị viên và Kiểm duyệt viên luôn có quyền truy cập mọi
|
||||
|
|
|
@ -49,6 +49,7 @@ sendMessage: "发送"
|
|||
copyUsername: "复制用户名"
|
||||
searchUser: "搜索用户"
|
||||
reply: "回复"
|
||||
replies: "回复"
|
||||
loadMore: "加载更多"
|
||||
showMore: "查看更多"
|
||||
showLess: "关闭"
|
||||
|
@ -94,11 +95,13 @@ unfollow: "取消关注"
|
|||
followRequestPending: "关注请求待批准"
|
||||
enterEmoji: "输入表情符号"
|
||||
renote: "转发"
|
||||
renotes: "转发"
|
||||
unrenote: "取消转发"
|
||||
renoted: "已转发。"
|
||||
cantRenote: "此帖子无法被转发。"
|
||||
cantReRenote: "转发无法被再次转发。"
|
||||
quote: "引用"
|
||||
quotes: "引用"
|
||||
pinnedNote: "已置顶的帖子"
|
||||
pinned: "置顶"
|
||||
you: "您"
|
||||
|
@ -106,6 +109,7 @@ clickToShow: "点击以显示"
|
|||
sensitive: "敏感内容"
|
||||
add: "添加"
|
||||
reaction: "回应"
|
||||
reactions: "回应"
|
||||
enableEmojiReaction: "启用表情符号回应"
|
||||
showEmojisInReactionNotifications: "在回应通知中显示表情符号"
|
||||
reactionSetting: "在回应选择器中显示的回应"
|
||||
|
@ -321,9 +325,6 @@ dayX: "{day} 日"
|
|||
monthX: "{month} 月"
|
||||
yearX: "{year} 年"
|
||||
pages: "页面"
|
||||
integration: "整合"
|
||||
connectService: "连接"
|
||||
disconnectService: "断开连接"
|
||||
enableLocalTimeline: "启用本地时间线功能"
|
||||
enableGlobalTimeline: "启用全局时间线"
|
||||
disablingTimelinesInfo: "管理员和监察员将始终拥有对所有时间线的访问权,即使它们没有被启用。"
|
||||
|
@ -936,7 +937,8 @@ _accountDelete:
|
|||
inProgress: "正在删除"
|
||||
_ad:
|
||||
back: "返回"
|
||||
reduceFrequencyOfThisAd: "减少此广告的频率"
|
||||
reduceFrequencyOfThisAd: "减少此横幅的频率"
|
||||
adsBy: 社区横幅(作者:{by})
|
||||
_forgotPassword:
|
||||
enterEmail: "请输入您注册账号时用的电子邮箱地址,密码重置链接将发送至该邮箱上。"
|
||||
ifNoEmail: "如果您在注册时没有输入电子邮件地址,请联系服务器管理员。"
|
||||
|
@ -1992,3 +1994,4 @@ indexable: 可索引的
|
|||
languageForTranslation: 帖子翻译语言
|
||||
vibrate: 播放振动
|
||||
openServerInfo: 点击帖子上的服务器滚动条时显示服务器信息
|
||||
clickToShowPatterns: 点击显示模块模式
|
||||
|
|
|
@ -21,7 +21,7 @@ basicSettings: "基本設定"
|
|||
otherSettings: "其他設定"
|
||||
openInWindow: "在新視窗開啟"
|
||||
profile: "個人檔案"
|
||||
timeline: "時間線"
|
||||
timeline: "時間軸"
|
||||
noAccountDescription: "此用戶還沒有自我介紹。"
|
||||
login: "登入"
|
||||
loggingIn: "登入中"
|
||||
|
@ -49,6 +49,7 @@ sendMessage: "發送訊息"
|
|||
copyUsername: "複製使用者名稱"
|
||||
searchUser: "搜尋使用者"
|
||||
reply: "回覆"
|
||||
replies: "回覆"
|
||||
loadMore: "載入更多"
|
||||
showMore: "載入更多"
|
||||
showLess: "關閉"
|
||||
|
@ -94,11 +95,13 @@ unfollow: "取消追隨"
|
|||
followRequestPending: "追隨許可批准中"
|
||||
enterEmoji: "輸入表情符號"
|
||||
renote: "轉發"
|
||||
renotes: "轉發"
|
||||
unrenote: "取消轉發"
|
||||
renoted: "已轉發。"
|
||||
cantRenote: "無法轉發此貼文。"
|
||||
cantReRenote: "無法轉發之前已經轉發過的內容。"
|
||||
quote: "引用"
|
||||
quotes: "引用"
|
||||
pinnedNote: "已置頂的貼文"
|
||||
pinned: "置頂"
|
||||
you: "您"
|
||||
|
@ -106,6 +109,7 @@ clickToShow: "按一下以顯示"
|
|||
sensitive: "敏感內容"
|
||||
add: "新增"
|
||||
reaction: "反應"
|
||||
reactions: "反應"
|
||||
enableEmojiReaction: "啟用表情符號反應"
|
||||
showEmojisInReactionNotifications: "在反應通知中顯示表情符號"
|
||||
reactionSetting: "在選擇器中顯示反應"
|
||||
|
@ -145,12 +149,12 @@ flagAsBot: "標記此帳號是機器人"
|
|||
flagAsBotDescription: "如果本帳戶是由程式控制,請啟用此選項。啟用後,會作為標示幫助其他開發者防止機器人之間產生無限互動的行為,並會調整Firefish內部系統將本帳戶識別為機器人。"
|
||||
flagAsCat: "你是喵咪嗎?w😺"
|
||||
flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示!"
|
||||
flagShowTimelineReplies: "在時間線上顯示貼文的回覆"
|
||||
flagShowTimelineRepliesDescription: "啟用時,時間線除了顯示用戶的貼文以外,還會顯示用戶對其他貼文的回覆。"
|
||||
flagShowTimelineReplies: "在時間軸上顯示貼文的回覆"
|
||||
flagShowTimelineRepliesDescription: "啟用時,時間軸除了顯示用戶的貼文以外,還會顯示用戶對其他貼文的回覆。"
|
||||
autoAcceptFollowed: "自動准予追隨中使用者的追隨請求"
|
||||
addAccount: "添加帳戶"
|
||||
loginFailed: "登入失敗"
|
||||
showOnRemote: "轉到所在伺服器顯示"
|
||||
showOnRemote: "開啟來源頁面"
|
||||
general: "一般"
|
||||
wallpaper: "桌布"
|
||||
setWallpaper: "設定桌布"
|
||||
|
@ -320,12 +324,9 @@ dayX: "{day}日"
|
|||
monthX: "{month}月"
|
||||
yearX: "{year}年"
|
||||
pages: "頁面"
|
||||
integration: "整合"
|
||||
connectService: "己連結"
|
||||
disconnectService: "己斷開"
|
||||
enableLocalTimeline: "開啟本地時間線"
|
||||
enableGlobalTimeline: "啟用公開時間線"
|
||||
disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和板主仍可訪問所有的時間線。"
|
||||
enableLocalTimeline: "開啟本地時間軸"
|
||||
enableGlobalTimeline: "啟用公開時間軸"
|
||||
disablingTimelinesInfo: "即使您關閉了時間軸功能,管理員和板主仍可訪問所有的時間軸。"
|
||||
registration: "註冊"
|
||||
enableRegistration: "開啟新使用者註冊"
|
||||
invite: "邀請"
|
||||
|
@ -385,7 +386,7 @@ administrator: "管理員"
|
|||
token: "權杖"
|
||||
twoStepAuthentication: "兩階段驗證"
|
||||
moderator: "板主"
|
||||
moderation: "言論調節"
|
||||
moderation: "管理"
|
||||
nUsersMentioned: "提到了{n}"
|
||||
securityKey: "安全金鑰"
|
||||
securityKeyName: "金鑰名稱"
|
||||
|
@ -482,7 +483,7 @@ promotion: "推廣"
|
|||
promote: "推廣"
|
||||
numberOfDays: "有效天數"
|
||||
hideThisNote: "隱藏此貼文"
|
||||
showFeaturedNotesInTimeline: "在時間線上顯示熱門推薦"
|
||||
showFeaturedNotesInTimeline: "在時間軸上顯示熱門推薦"
|
||||
objectStorage: "Object Storage (物件儲存)"
|
||||
useObjectStorage: "使用Object Storage"
|
||||
objectStorageBaseUrl: "根URL"
|
||||
|
@ -502,7 +503,7 @@ objectStorageUseProxyDesc: "如果不使用代理進行API連接,請關閉"
|
|||
objectStorageSetPublicRead: "上傳時設定為\"public-read\""
|
||||
serverLogs: "伺服器日誌"
|
||||
deleteAll: "刪除所有記錄"
|
||||
showFixedPostForm: "於時間線頁頂顯示「發送貼文」方框"
|
||||
showFixedPostForm: "於時間軸頁頂顯示「發送貼文」方框"
|
||||
newNoteRecived: "發現新的貼文"
|
||||
sounds: "音效"
|
||||
listen: "聆聽"
|
||||
|
@ -670,7 +671,7 @@ no: "取消"
|
|||
driveFilesCount: "雲端硬碟檔案數量"
|
||||
driveUsage: "雲端硬碟使用量"
|
||||
noCrawle: "拒絕搜尋引擎索引"
|
||||
noCrawleDescription: "要求網路搜尋引擎不要索引你的個人資料頁、貼文及頁面等。"
|
||||
noCrawleDescription: "要求外部搜尋引擎不要收錄(索引)你的內容(個人檔案、貼文、頁面等)。"
|
||||
lockedAccountInfo: "即使你通過了追隨者請求,除非你將貼文的可見性設定為 「追隨者」,否則任何人都能看見你的貼文。"
|
||||
alwaysMarkSensitive: "默認將圖像/影像標記為敏感內容"
|
||||
loadRawImages: "以原始圖檔顯示附件圖檔的縮圖"
|
||||
|
@ -688,7 +689,7 @@ experimentalFeatures: "實驗中的功能"
|
|||
developer: "開發者"
|
||||
makeExplorable: "使自己的帳戶能夠在“探索”頁面中顯示"
|
||||
makeExplorableDescription: "如果關閉,帳戶將不會被顯示在\"探索\"頁面中。"
|
||||
showGapBetweenNotesInTimeline: "分開顯示時間線上的貼文"
|
||||
showGapBetweenNotesInTimeline: "分開顯示時間軸上的貼文"
|
||||
duplicate: "複製"
|
||||
left: "左"
|
||||
center: "置中"
|
||||
|
@ -734,7 +735,7 @@ inChannelSearch: "頻道内搜尋"
|
|||
useReactionPickerForContextMenu: "點擊右鍵開啟反應工具欄"
|
||||
typingUsers: "{users}輸入中"
|
||||
jumpToSpecifiedDate: "跳轉到特定日期"
|
||||
showingPastTimeline: "顯示過往的時間線"
|
||||
showingPastTimeline: "顯示過往的時間軸"
|
||||
clear: "清除"
|
||||
markAllAsRead: "全部標示為已讀"
|
||||
goBack: "返回"
|
||||
|
@ -786,7 +787,7 @@ previewNoteText: "預覽文本"
|
|||
customCss: "自定義 CSS"
|
||||
customCssWarn: "這個設定必須由具備相關知識的人員操作,不當的設定可能导致客戶端無法正常使用。"
|
||||
global: "公開"
|
||||
squareAvatars: "頭像以方形顯示"
|
||||
squareAvatars: "大頭貼以方形顯示"
|
||||
sent: "發送"
|
||||
received: "收取"
|
||||
searchResult: "搜尋結果"
|
||||
|
@ -822,7 +823,7 @@ unmuteThread: "將貼文串的靜音解除"
|
|||
ffVisibility: "連接的公開範圍"
|
||||
ffVisibilityDescription: "您可以設定您的關注/關注者資訊的公開範圍。"
|
||||
continueThread: "查看更多貼文"
|
||||
deleteAccountConfirm: "將要刪除帳戶。是否確定?"
|
||||
deleteAccountConfirm: "此帳戶將被刪除,是否繼續?"
|
||||
incorrectPassword: "密碼錯誤。"
|
||||
voteConfirm: "確定投給「{choice}」?"
|
||||
hide: "隱藏"
|
||||
|
@ -933,6 +934,7 @@ _accountDelete:
|
|||
_ad:
|
||||
back: "返回"
|
||||
reduceFrequencyOfThisAd: "降低此橫幅的頻率"
|
||||
adsBy: 社群橫幅(作者:{by})
|
||||
_forgotPassword:
|
||||
enterEmail: "請輸入您的帳戶註冊的電子郵件地址。 密碼重置連結將被發送到該電子郵件地址。"
|
||||
ifNoEmail: "如果您還沒有註冊您的電子郵件地址,請聯繫管理員。"
|
||||
|
@ -1109,14 +1111,14 @@ _wordMute:
|
|||
muteWords: "加入靜音文字"
|
||||
muteWordsDescription: "用空格分隔指定AND,用換行分隔指定OR。"
|
||||
muteWordsDescription2: "將關鍵字用斜線括起來表示正規表達式。"
|
||||
softDescription: "隱藏時間線中指定條件的貼文。"
|
||||
hardDescription: "具有指定條件的貼文將不添加到時間線。 即使您更改條件,未被添加的貼文也會被排除在外。"
|
||||
softDescription: "隱藏時間軸中指定條件的貼文。"
|
||||
hardDescription: "符合指定條件的貼文將不添加到時間軸。 即使您更改條件,未被添加的貼文也會被排除在外。"
|
||||
soft: "軟性靜音"
|
||||
hard: "硬性靜音"
|
||||
mutedNotes: "已靜音的貼文"
|
||||
muteLangsDescription2: '使用語言代碼。例: en, fr, ja, zh.'
|
||||
lang: 語言
|
||||
langDescription: 將指定語言的貼文從時間線中隱藏。
|
||||
langDescription: 將指定語言的貼文從時間軸中隱藏。
|
||||
muteLangs: 被靜音的語言
|
||||
muteLangsDescription: OR條件以空格或換行進行分隔。
|
||||
_instanceMute:
|
||||
|
@ -1228,16 +1230,16 @@ _tutorial:
|
|||
step2_1: "首先,請完成你的個人資料。"
|
||||
step2_2: "通過提供一些關於你自己的資料,其他人會更容易了解他們是否想看到你的貼文或關注你。"
|
||||
step3_1: "現在是時候追隨一些人了!"
|
||||
step3_2: "你的主頁和社交時間線是基於你所追蹤的人,所以試著先追蹤幾個帳戶。\n點擊個人資料右上角的加號圈就可以關注它。"
|
||||
step3_2: "你的主頁和社交時間軸是基於你所追蹤的人,所以試著先追蹤幾個帳戶。\n點擊個人資料右上角的加號按鈕就可以關注它。"
|
||||
step4_1: "讓我們出去找你。"
|
||||
step4_2: "作為第一則貼文,有些人喜歡發 {introduction} 或單純發一個 \"hello world!\""
|
||||
step5_1: "時間線,到處都是時間線!"
|
||||
step5_2: "您的伺服器已啟用了{timelines}個時間線。"
|
||||
step5_3: "首頁 {icon} 時間線是顯示你追蹤的帳號的貼文。"
|
||||
step5_4: "本地 {icon} 時間線是你可以看到伺服器中所有其他用戶的貼文的時間線。"
|
||||
step5_5: "社交 {icon} 時間線是你的 首頁時間線 和 本地時間線 的結合體。"
|
||||
step5_6: "推薦 {icon} 時間線是顯示你的伺服器管理員推薦的貼文。"
|
||||
step5_7: "全球 {icon} 時間線是顯示來自所有其他連接的伺服器的貼文。"
|
||||
step5_1: "時間軸,到處都是時間軸!"
|
||||
step5_2: "您的伺服器已啟用了{timelines}個時間軸。"
|
||||
step5_3: "首頁 {icon} 時間軸是顯示你追蹤的帳號的貼文。"
|
||||
step5_4: "本地 {icon} 時間軸是你可以看到伺服器中所有其他用戶的貼文的時間軸。"
|
||||
step5_5: "社交 {icon} 時間軸是你的 首頁時間軸 和 本地時間軸 的結合體。"
|
||||
step5_6: "推薦 {icon} 時間軸是顯示你的伺服器管理員推薦的貼文。"
|
||||
step5_7: "全球 {icon} 時間軸是顯示來自所有其他連接的伺服器的貼文。"
|
||||
step6_1: "那麼,這裡是什麼地方?"
|
||||
step6_2: "你不只是加入Firefish。你已經加入了Fediverse的一個門戶,這是一個由成千上萬台服務器組成的互聯網絡。"
|
||||
step6_3: "每個服務器也有不同,而並不是所有的服務器都運行Firefish。但這個服務器確實是運行Firefish的! 你可能會覺得有點複雜,但你很快就會明白的。"
|
||||
|
@ -1326,7 +1328,7 @@ _weekday:
|
|||
_widgets:
|
||||
memo: "備忘錄"
|
||||
notifications: "通知"
|
||||
timeline: "時間線"
|
||||
timeline: "時間軸"
|
||||
calendar: "行事曆"
|
||||
trends: "發燒貼文"
|
||||
clock: "時鐘"
|
||||
|
@ -1382,9 +1384,9 @@ _poll:
|
|||
remainingSeconds: "{s}秒後截止"
|
||||
_visibility:
|
||||
public: "公開"
|
||||
publicDescription: "發佈至公開時間線"
|
||||
publicDescription: "發佈至公開時間軸"
|
||||
home: "不在主頁顯示"
|
||||
homeDescription: "僅發送至首頁的時間線"
|
||||
homeDescription: "僅發送至首頁的時間軸"
|
||||
followers: "追隨者"
|
||||
followersDescription: "僅發佈至關注者"
|
||||
specified: "指定使用者"
|
||||
|
@ -1783,6 +1785,7 @@ _notification:
|
|||
renote: "轉發"
|
||||
reacted: 對您的貼文做出了反應
|
||||
renoted: 轉發了您的貼文
|
||||
voted: 投了票
|
||||
_deck:
|
||||
alwaysShowMainColumn: "總是顯示主欄"
|
||||
columnAlign: "對齊欄位"
|
||||
|
@ -1806,7 +1809,7 @@ _deck:
|
|||
main: "主列"
|
||||
widgets: "小工具"
|
||||
notifications: "通知"
|
||||
tl: "時間線"
|
||||
tl: "時間軸"
|
||||
antenna: "天線"
|
||||
list: "清單"
|
||||
mentions: "提及"
|
||||
|
@ -1838,7 +1841,7 @@ accountMoved: '該使用者已遷移至新帳戶:'
|
|||
showAds: 顯示社群橫幅
|
||||
noThankYou: 不用了,謝謝
|
||||
selectInstance: 選擇伺服器
|
||||
enableRecommendedTimeline: 啟用推薦時間線
|
||||
enableRecommendedTimeline: 啟用推薦時間軸
|
||||
antennaInstancesDescription: 分行列出一個伺服器
|
||||
moveTo: 遷移此帳戶到新帳戶
|
||||
moveToLabel: '請輸入你將會遷移到的帳戶:'
|
||||
|
@ -1870,6 +1873,7 @@ silenced: 已靜音
|
|||
_experiments:
|
||||
title: 試驗功能
|
||||
enablePostImports: 啟用匯入貼文的功能
|
||||
postImportsCaption: 允許用戶從舊有的Firefish・Misskey・Mastodon・Akkoma・Pleroma帳號匯入貼文。在伺服器佇列堵塞時匯入貼文可能會導致載入速度變慢。
|
||||
findOtherInstance: 找找另一個伺服器
|
||||
noGraze: 瀏覽器擴充元件 "Graze for Mastodon" 會與Firefish發生衝突,請停用該擴充元件。
|
||||
userSaysSomethingReasonRenote: '{name} 轉發了包含 {reason} 的貼文'
|
||||
|
@ -1891,7 +1895,7 @@ pushNotification: 推送通知
|
|||
subscribePushNotification: 啟用推送通知
|
||||
unsubscribePushNotification: 停用推送通知
|
||||
pushNotificationAlreadySubscribed: 推送通知已經啟用
|
||||
recommendedInstancesDescription: 以每行分隔的推薦伺服器出現在推薦的時間線中。
|
||||
recommendedInstancesDescription: 推薦的伺服器(將顯示在推薦時間軸中),一行一個。
|
||||
searchPlaceholder: 在 Firefish 上搜尋
|
||||
cw: 內容警告
|
||||
selectChannel: 選擇一個頻道
|
||||
|
@ -1899,9 +1903,9 @@ newer: 較新
|
|||
older: 較舊
|
||||
jumpToPrevious: 跳到上一個
|
||||
removeReaction: 移除你的反應
|
||||
listsDesc: 清單可以創建一個只有您指定用戶的時間線。 可以從時間線頁面訪問它們。
|
||||
listsDesc: 清單可以創建一個只有您指定用戶的時間軸。 可以從時間軸頁面訪問它們。
|
||||
flagSpeakAsCatDescription: 在喵咪模式下你的貼文會被喵化ヾ(•ω•`)o
|
||||
antennasDesc: "天線會顯示符合您設置條件的新貼文!\n 可以從時間線訪問它們。"
|
||||
antennasDesc: "天線會顯示符合您設置條件的新貼文!\n 可以從時間軸訪問它們。"
|
||||
expandOnNoteClick: 點擊以打開貼文
|
||||
expandOnNoteClickDesc: 即使停用,您仍然可以從右鍵選單或單擊發文時間來打開貼文。
|
||||
hiddenTagsDescription: '列出您希望隱藏趨勢和探索的主題標籤(不帶 #)。 隱藏的主題標籤仍然可以通過其他方式發現。'
|
||||
|
@ -1920,7 +1924,7 @@ seperateRenoteQuote: 分開轉發及引用的按鈕
|
|||
clipsDesc: 摘錄就像一個可以分享的書籤。 你可以從每個貼文的菜單創建新摘錄或將貼文加入已有的摘錄。
|
||||
noteId: 貼文 ID
|
||||
sendModMail: 發送審核通知
|
||||
enableIdenticonGeneration: 啟用碎片生成
|
||||
enableIdenticonGeneration: 啟用Identicon生成
|
||||
enableServerMachineStats: 啟用伺服器硬體統計資訊
|
||||
reactionPickerSkinTone: 首選表情符號膚色
|
||||
indexFromDescription: 留空以索引每個貼文
|
||||
|
@ -1961,10 +1965,17 @@ _dialog:
|
|||
charactersExceeded: 超過字數限制! 當前 {current} / 限制 {max}
|
||||
_skinTones:
|
||||
yellow: 黃色
|
||||
medium: 中等
|
||||
dark: 深色
|
||||
mediumDark: 中等偏深
|
||||
light: 淺色
|
||||
mediumLight: 中等偏淺
|
||||
exportZip: 匯出ZIP
|
||||
_feeds:
|
||||
atom: Atom
|
||||
rss: RSS
|
||||
copyFeed: 複製訂閱URL
|
||||
jsonFeed: JSON Feed
|
||||
emojiPackCreator: 表情包的作者
|
||||
importZip: 匯入ZIP
|
||||
delete2fa: 停用二階段認證(2FA)
|
||||
|
@ -1977,3 +1988,9 @@ addRe: 在回覆有內容警告的貼文時,在標題前面加上 "re:"
|
|||
vibrate: 播放振動
|
||||
openServerInfo: 點擊貼文中的伺服器名稱以顯示伺服器資訊
|
||||
languageForTranslation: 貼文翻譯語言
|
||||
objectStorageS3ForcePathStyleDesc: 以 "s3.amazonaws.com/<bucket>/" 而非 "<bucket>.s3.amazonaws.com"
|
||||
的格式建構端點(Endpoint)URL。
|
||||
indexable: 登錄至貼文搜尋引擎
|
||||
origin: 來源
|
||||
objectStorageS3ForcePathStyle: 使用基於路徑的端點(Endpoint)URL
|
||||
clickToShowPatterns: 點擊顯示模組模式(Module Pattern)
|
||||
|
|
|
@ -7,6 +7,7 @@ mod m20230627_185451_index_note_url;
|
|||
mod m20230709_000510_move_antenna_to_cache;
|
||||
mod m20230806_170616_fix_antenna_stream_ids;
|
||||
mod m20230904_013244_is_indexable;
|
||||
mod m20231002_143323_remove_integrations;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
|
@ -19,6 +20,7 @@ impl MigratorTrait for Migrator {
|
|||
Box::new(m20230709_000510_move_antenna_to_cache::Migration),
|
||||
Box::new(m20230806_170616_fix_antenna_stream_ids::Migration),
|
||||
Box::new(m20230904_013244_is_indexable::Migration),
|
||||
Box::new(m20231002_143323_remove_integrations::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(UserProfile::Table)
|
||||
.drop_column(UserProfile::Integrations)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Meta::Table)
|
||||
.drop_column(Meta::EnableTwitterIntegration)
|
||||
.drop_column(Meta::TwitterConsumerKey)
|
||||
.drop_column(Meta::TwitterConsumerSecret)
|
||||
.drop_column(Meta::EnableGithubIntegration)
|
||||
.drop_column(Meta::GithubClientId)
|
||||
.drop_column(Meta::GithubClientSecret)
|
||||
.drop_column(Meta::EnableDiscordIntegration)
|
||||
.drop_column(Meta::DiscordClientId)
|
||||
.drop_column(Meta::DiscordClientSecret)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Meta::Table)
|
||||
.add_column(ColumnDef::new(Meta::DiscordClientSecret).string())
|
||||
.add_column(ColumnDef::new(Meta::DiscordClientId).string())
|
||||
.add_column(
|
||||
ColumnDef::new(Meta::EnableDiscordIntegration)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false),
|
||||
)
|
||||
.add_column(ColumnDef::new(Meta::GithubClientSecret).string())
|
||||
.add_column(ColumnDef::new(Meta::GithubClientId).string())
|
||||
.add_column(
|
||||
ColumnDef::new(Meta::EnableGithubIntegration)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false),
|
||||
)
|
||||
.add_column(ColumnDef::new(Meta::TwitterConsumerSecret).string())
|
||||
.add_column(ColumnDef::new(Meta::TwitterConsumerKey).string())
|
||||
.add_column(
|
||||
ColumnDef::new(Meta::EnableTwitterIntegration)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(UserProfile::Table)
|
||||
.add_column(
|
||||
ColumnDef::new(UserProfile::Integrations)
|
||||
.json()
|
||||
.default("{}"),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
|
||||
enum UserProfile {
|
||||
Table,
|
||||
#[iden = "integrations"]
|
||||
Integrations,
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
enum Meta {
|
||||
Table,
|
||||
#[iden = "enableTwitterIntegration"]
|
||||
EnableTwitterIntegration,
|
||||
#[iden = "twitterConsumerKey"]
|
||||
TwitterConsumerKey,
|
||||
#[iden = "twitterConsumerSecret"]
|
||||
TwitterConsumerSecret,
|
||||
#[iden = "enableGithubIntegration"]
|
||||
EnableGithubIntegration,
|
||||
#[iden = "githubClientId"]
|
||||
GithubClientId,
|
||||
#[iden = "githubClientSecret"]
|
||||
GithubClientSecret,
|
||||
#[iden = "enableDiscordIntegration"]
|
||||
EnableDiscordIntegration,
|
||||
#[iden = "discordClientId"]
|
||||
DiscordClientId,
|
||||
#[iden = "discordClientSecret"]
|
||||
DiscordClientSecret,
|
||||
}
|
|
@ -69,24 +69,6 @@ pub struct Model {
|
|||
pub sw_public_key: Option<String>,
|
||||
#[sea_orm(column_name = "swPrivateKey")]
|
||||
pub sw_private_key: Option<String>,
|
||||
#[sea_orm(column_name = "enableTwitterIntegration")]
|
||||
pub enable_twitter_integration: bool,
|
||||
#[sea_orm(column_name = "twitterConsumerKey")]
|
||||
pub twitter_consumer_key: Option<String>,
|
||||
#[sea_orm(column_name = "twitterConsumerSecret")]
|
||||
pub twitter_consumer_secret: Option<String>,
|
||||
#[sea_orm(column_name = "enableGithubIntegration")]
|
||||
pub enable_github_integration: bool,
|
||||
#[sea_orm(column_name = "githubClientId")]
|
||||
pub github_client_id: Option<String>,
|
||||
#[sea_orm(column_name = "githubClientSecret")]
|
||||
pub github_client_secret: Option<String>,
|
||||
#[sea_orm(column_name = "enableDiscordIntegration")]
|
||||
pub enable_discord_integration: bool,
|
||||
#[sea_orm(column_name = "discordClientId")]
|
||||
pub discord_client_id: Option<String>,
|
||||
#[sea_orm(column_name = "discordClientSecret")]
|
||||
pub discord_client_secret: Option<String>,
|
||||
#[sea_orm(column_name = "pinnedUsers")]
|
||||
pub pinned_users: Vec<String>,
|
||||
#[sea_orm(column_name = "ToSUrl")]
|
||||
|
|
|
@ -45,8 +45,6 @@ pub struct Model {
|
|||
pub pinned_page_id: Option<String>,
|
||||
#[sea_orm(column_type = "JsonBinary")]
|
||||
pub room: Json,
|
||||
#[sea_orm(column_type = "JsonBinary")]
|
||||
pub integrations: Json,
|
||||
#[sea_orm(column_name = "injectFeaturedNote")]
|
||||
pub inject_featured_note: bool,
|
||||
#[sea_orm(column_name = "enableWordMute")]
|
||||
|
|
|
@ -100,7 +100,6 @@
|
|||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "6.9.4",
|
||||
"nsfwjs": "2.4.2",
|
||||
"oauth": "^0.10.0",
|
||||
"opencc-js": "^1.0.5",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "^9.1.4",
|
||||
|
|
|
@ -1,76 +1,29 @@
|
|||
import type { Antenna } from "@/models/entities/antenna.js";
|
||||
import type { Note } from "@/models/entities/note.js";
|
||||
import type { User } from "@/models/entities/user.js";
|
||||
import {
|
||||
UserListJoinings,
|
||||
UserGroupJoinings,
|
||||
Blockings,
|
||||
} from "@/models/index.js";
|
||||
import { Blockings, UserProfiles } from "@/models/index.js";
|
||||
import { getFullApAccount } from "./convert-host.js";
|
||||
import * as Acct from "@/misc/acct.js";
|
||||
import type { Packed } from "./schema.js";
|
||||
import { Cache } from "./cache.js";
|
||||
import { getWordHardMute } from "./check-word-mute.js";
|
||||
|
||||
const blockingCache = new Cache<User["id"][]>("blocking", 60 * 5);
|
||||
const mutedWordsCache = new Cache<string[][] | undefined>("mutedWords", 60 * 5);
|
||||
|
||||
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
||||
|
||||
/**
|
||||
* noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい
|
||||
*/
|
||||
export async function checkHitAntenna(
|
||||
antenna: Antenna,
|
||||
note: Note | Packed<"Note">,
|
||||
noteUser: { id: User["id"]; username: string; host: string | null },
|
||||
noteUserFollowers?: User["id"][],
|
||||
antennaUserFollowing?: User["id"][],
|
||||
): Promise<boolean> {
|
||||
if (note.visibility === "specified") return false;
|
||||
if (note.visibility === "home") return false;
|
||||
|
||||
// アンテナ作成者がノート作成者にブロックされていたらスキップ
|
||||
const blockings = await blockingCache.fetch(noteUser.id, () =>
|
||||
Blockings.findBy({ blockerId: noteUser.id }).then((res) =>
|
||||
res.map((x) => x.blockeeId),
|
||||
),
|
||||
);
|
||||
if (blockings.some((blocking) => blocking === antenna.userId)) return false;
|
||||
|
||||
if (note.visibility === "followers") {
|
||||
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId))
|
||||
return false;
|
||||
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId))
|
||||
return false;
|
||||
if (!antenna.withReplies && note.replyId != null) return false;
|
||||
if (antenna.withFile) {
|
||||
if (note.fileIds && note.fileIds.length === 0) return false;
|
||||
}
|
||||
|
||||
if (!antenna.withReplies && note.replyId != null) return false;
|
||||
|
||||
if (antenna.src === "home") {
|
||||
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId))
|
||||
return false;
|
||||
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId))
|
||||
return false;
|
||||
} else if (antenna.src === "list") {
|
||||
const listUsers = (
|
||||
await UserListJoinings.findBy({
|
||||
userListId: antenna.userListId!,
|
||||
})
|
||||
).map((x) => x.userId);
|
||||
|
||||
if (!listUsers.includes(note.userId)) return false;
|
||||
} else if (antenna.src === "group") {
|
||||
const joining = await UserGroupJoinings.findOneByOrFail({
|
||||
id: antenna.userGroupJoiningId!,
|
||||
});
|
||||
|
||||
const groupUsers = (
|
||||
await UserGroupJoinings.findBy({
|
||||
userGroupId: joining.userGroupId,
|
||||
})
|
||||
).map((x) => x.userId);
|
||||
|
||||
if (!groupUsers.includes(note.userId)) return false;
|
||||
} else if (antenna.src === "users") {
|
||||
if (antenna.src === "users") {
|
||||
const accts = antenna.users.map((x) => {
|
||||
const { username, host } = Acct.parse(x);
|
||||
return getFullApAccount(username, host).toLowerCase();
|
||||
|
@ -128,9 +81,20 @@ export async function checkHitAntenna(
|
|||
if (matched) return false;
|
||||
}
|
||||
|
||||
if (antenna.withFile) {
|
||||
if (note.fileIds && note.fileIds.length === 0) return false;
|
||||
}
|
||||
// アンテナ作成者がノート作成者にブロックされていたらスキップ
|
||||
const blockings = await blockingCache.fetch(noteUser.id, () =>
|
||||
Blockings.findBy({ blockerId: noteUser.id }).then((res) =>
|
||||
res.map((x) => x.blockeeId),
|
||||
),
|
||||
);
|
||||
if (blockings.includes(antenna.userId)) return false;
|
||||
|
||||
const mutedWords = await mutedWordsCache.fetch(antenna.userId, () =>
|
||||
UserProfiles.findOneBy({ userId: antenna.userId }).then(
|
||||
(profile) => profile?.mutedWords,
|
||||
),
|
||||
);
|
||||
if (await getWordHardMute(note, antenna.userId, mutedWords)) return false;
|
||||
|
||||
// TODO: eval expression
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import RE2 from "re2";
|
||||
import type { Note } from "@/models/entities/note.js";
|
||||
import type { User } from "@/models/entities/user.js";
|
||||
import { DriveFile } from "@/models/entities/drive-file";
|
||||
import { scyllaClient, type ScyllaNote } from "@/db/scylla.js";
|
||||
|
||||
|
@ -11,10 +10,6 @@ type NoteLike = {
|
|||
cw?: Note["cw"];
|
||||
};
|
||||
|
||||
type UserLike = {
|
||||
id: User["id"];
|
||||
};
|
||||
|
||||
function checkWordMute(
|
||||
note: NoteLike | ScyllaNote,
|
||||
mutedWords: Array<string | string[]>,
|
||||
|
@ -77,15 +72,12 @@ function checkWordMute(
|
|||
return false;
|
||||
}
|
||||
|
||||
export function getWordHardMute(
|
||||
note: NoteLike | ScyllaNote,
|
||||
me: UserLike | null | undefined,
|
||||
mutedWords: Array<string | string[]>,
|
||||
): boolean {
|
||||
// 自分自身
|
||||
if (me && note.userId === me.id) {
|
||||
return false;
|
||||
}
|
||||
export async function getWordHardMute(
|
||||
note: NoteLike,
|
||||
meId: string | null | undefined,
|
||||
mutedWords?: Array<string | string[]>,
|
||||
): Promise<boolean> {
|
||||
if (note.userId === meId || mutedWords == null) return false;
|
||||
|
||||
let ng = false;
|
||||
|
||||
|
|
|
@ -354,57 +354,6 @@ export class Meta {
|
|||
})
|
||||
public swPrivateKey: string | null;
|
||||
|
||||
@Column("boolean", {
|
||||
default: false,
|
||||
})
|
||||
public enableTwitterIntegration: boolean;
|
||||
|
||||
@Column("varchar", {
|
||||
length: 128,
|
||||
nullable: true,
|
||||
})
|
||||
public twitterConsumerKey: string | null;
|
||||
|
||||
@Column("varchar", {
|
||||
length: 128,
|
||||
nullable: true,
|
||||
})
|
||||
public twitterConsumerSecret: string | null;
|
||||
|
||||
@Column("boolean", {
|
||||
default: false,
|
||||
})
|
||||
public enableGithubIntegration: boolean;
|
||||
|
||||
@Column("varchar", {
|
||||
length: 128,
|
||||
nullable: true,
|
||||
})
|
||||
public githubClientId: string | null;
|
||||
|
||||
@Column("varchar", {
|
||||
length: 128,
|
||||
nullable: true,
|
||||
})
|
||||
public githubClientSecret: string | null;
|
||||
|
||||
@Column("boolean", {
|
||||
default: false,
|
||||
})
|
||||
public enableDiscordIntegration: boolean;
|
||||
|
||||
@Column("varchar", {
|
||||
length: 128,
|
||||
nullable: true,
|
||||
})
|
||||
public discordClientId: string | null;
|
||||
|
||||
@Column("varchar", {
|
||||
length: 128,
|
||||
nullable: true,
|
||||
})
|
||||
public discordClientSecret: string | null;
|
||||
|
||||
@Column("varchar", {
|
||||
length: 128,
|
||||
nullable: true,
|
||||
|
|
|
@ -215,11 +215,6 @@ export class UserProfile {
|
|||
@JoinColumn()
|
||||
public pinnedPage: Page | null;
|
||||
|
||||
@Column("jsonb", {
|
||||
default: {},
|
||||
})
|
||||
public integrations: Record<string, any>;
|
||||
|
||||
@Index()
|
||||
@Column("boolean", {
|
||||
default: false,
|
||||
|
|
|
@ -599,7 +599,6 @@ export const UserRepository = db.getRepository(User).extend({
|
|||
hasUnreadNotification: this.getHasUnreadNotification(user.id),
|
||||
hasPendingReceivedFollowRequest:
|
||||
this.getHasPendingReceivedFollowRequest(user.id),
|
||||
integrations: profile!.integrations,
|
||||
mutedWords: profile!.mutedWords,
|
||||
mutedInstances: profile!.mutedInstances,
|
||||
mutingNotificationTypes: profile!.mutingNotificationTypes,
|
||||
|
|
|
@ -459,11 +459,6 @@ export const packedMeDetailedOnlySchema = {
|
|||
nullable: false,
|
||||
optional: false,
|
||||
},
|
||||
integrations: {
|
||||
type: "object",
|
||||
nullable: true,
|
||||
optional: false,
|
||||
},
|
||||
mutedWords: {
|
||||
type: "array",
|
||||
nullable: false,
|
||||
|
|
|
@ -95,11 +95,25 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
|||
}
|
||||
|
||||
// HTTP-Signatureの検証
|
||||
const httpSignatureValidated = httpSignature.verifySignature(
|
||||
let httpSignatureValidated = httpSignature.verifySignature(
|
||||
signature,
|
||||
authUser.key.keyPem,
|
||||
);
|
||||
|
||||
// If signature validation failed, try refetching the actor
|
||||
if (!httpSignatureValidated) {
|
||||
authUser.key = await dbResolver.refetchPublicKeyForApId(authUser.user);
|
||||
|
||||
if (authUser.key == null) {
|
||||
return "skip: failed to re-resolve user publicKey";
|
||||
}
|
||||
|
||||
httpSignatureValidated = httpSignature.verifySignature(
|
||||
signature,
|
||||
authUser.key.keyPem,
|
||||
);
|
||||
}
|
||||
|
||||
// また、signatureのsignerは、activity.actorと一致する必要がある
|
||||
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
|
||||
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
|
||||
|
|
|
@ -86,11 +86,25 @@ export async function checkFetch(req: IncomingMessage): Promise<number> {
|
|||
}
|
||||
|
||||
// HTTP-Signatureの検証
|
||||
const httpSignatureValidated = httpSignature.verifySignature(
|
||||
let httpSignatureValidated = httpSignature.verifySignature(
|
||||
signature,
|
||||
authUser.key.keyPem,
|
||||
);
|
||||
|
||||
// If signature validation failed, try refetching the actor
|
||||
if (!httpSignatureValidated) {
|
||||
authUser.key = await dbResolver.refetchPublicKeyForApId(authUser.user);
|
||||
|
||||
if (authUser.key == null) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
httpSignatureValidated = httpSignature.verifySignature(
|
||||
signature,
|
||||
authUser.key.keyPem,
|
||||
);
|
||||
}
|
||||
|
||||
if (!httpSignatureValidated) {
|
||||
return 403;
|
||||
}
|
||||
|
|
|
@ -17,9 +17,10 @@ import { Cache } from "@/misc/cache.js";
|
|||
import { uriPersonCache, userByIdCache } from "@/services/user-cache.js";
|
||||
import type { IObject } from "./type.js";
|
||||
import { getApId } from "./type.js";
|
||||
import { resolvePerson } from "./models/person.js";
|
||||
import { resolvePerson, updatePerson } from "./models/person.js";
|
||||
import { parseScyllaNote, prepared, scyllaClient } from "@/db/scylla.js";
|
||||
|
||||
|
||||
const publicKeyCache = new Cache<UserPublickey | null>("publicKey", 60 * 30);
|
||||
const publicKeyByUserIdCache = new Cache<UserPublickey | null>(
|
||||
"publicKeyByUserId",
|
||||
|
@ -182,7 +183,7 @@ export default class DbResolver {
|
|||
*/
|
||||
public async getAuthUserFromKeyId(keyId: string): Promise<{
|
||||
user: CacheableRemoteUser;
|
||||
key: UserPublickey;
|
||||
key: UserPublickey | null;
|
||||
} | null> {
|
||||
const key = await publicKeyCache.fetch(
|
||||
keyId,
|
||||
|
@ -234,4 +235,13 @@ export default class DbResolver {
|
|||
key,
|
||||
};
|
||||
}
|
||||
|
||||
public async refetchPublicKeyForApId(user: CacheableRemoteUser): Promise<UserPublickey | null> {
|
||||
await updatePerson(user.uri!, undefined, undefined, user);
|
||||
let key = await UserPublickeys.findOneBy({ userId: user.id });
|
||||
if (key != null) {
|
||||
await publicKeyByUserIdCache.set(user.id, key);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -189,7 +189,7 @@ export async function createPerson(
|
|||
|
||||
const host = toPuny(new URL(object.id).hostname);
|
||||
|
||||
const { fields } = analyzeAttachments(person.attachment || []);
|
||||
const fields = analyzeAttachments(person.attachment || []);
|
||||
|
||||
const tags = extractApHashtags(person.tag)
|
||||
.map((tag) => normalizeForSearch(tag))
|
||||
|
@ -661,39 +661,6 @@ export async function resolvePerson(
|
|||
return await createPerson(uri, resolver);
|
||||
}
|
||||
|
||||
const services: {
|
||||
[x: string]: (id: string, username: string) => any;
|
||||
} = {
|
||||
"misskey:authentication:twitter": (userId, screenName) => ({
|
||||
userId,
|
||||
screenName,
|
||||
}),
|
||||
"misskey:authentication:github": (id, login) => ({ id, login }),
|
||||
"misskey:authentication:discord": (id, name) => $discord(id, name),
|
||||
};
|
||||
|
||||
const $discord = (id: string, name: string) => {
|
||||
if (typeof name !== "string") {
|
||||
name = "unknown#0000";
|
||||
}
|
||||
const [username, discriminator] = name.split("#");
|
||||
return { id, username, discriminator };
|
||||
};
|
||||
|
||||
function addService(target: { [x: string]: any }, source: IApPropertyValue) {
|
||||
const service = services[source.name];
|
||||
|
||||
if (typeof source.value !== "string") {
|
||||
source.value = "unknown";
|
||||
}
|
||||
|
||||
const [id, username] = source.value.split("@");
|
||||
|
||||
if (service) {
|
||||
target[source.name.split(":")[2]] = service(id, username);
|
||||
}
|
||||
}
|
||||
|
||||
export function analyzeAttachments(
|
||||
attachments: IObject | IObject[] | undefined,
|
||||
) {
|
||||
|
@ -701,22 +668,17 @@ export function analyzeAttachments(
|
|||
name: string;
|
||||
value: string;
|
||||
}[] = [];
|
||||
const services: { [x: string]: any } = {};
|
||||
|
||||
if (Array.isArray(attachments)) {
|
||||
for (const attachment of attachments.filter(isPropertyValue)) {
|
||||
if (isPropertyValue(attachment.identifier)) {
|
||||
addService(services, attachment.identifier);
|
||||
} else {
|
||||
fields.push({
|
||||
name: attachment.name,
|
||||
value: fromHtml(attachment.value),
|
||||
});
|
||||
}
|
||||
fields.push({
|
||||
name: attachment.name,
|
||||
value: fromHtml(attachment.value),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { fields, services };
|
||||
return fields;
|
||||
}
|
||||
|
||||
export async function updateFeatured(userId: User["id"], resolver?: Resolver) {
|
||||
|
|
|
@ -170,21 +170,6 @@ export const meta = {
|
|||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
enableTwitterIntegration: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
enableGithubIntegration: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
enableDiscordIntegration: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
enableServiceWorker: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
|
@ -326,36 +311,6 @@ export const meta = {
|
|||
nullable: true,
|
||||
format: "id",
|
||||
},
|
||||
twitterConsumerKey: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true,
|
||||
},
|
||||
twitterConsumerSecret: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true,
|
||||
},
|
||||
githubClientId: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true,
|
||||
},
|
||||
githubClientSecret: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true,
|
||||
},
|
||||
discordClientId: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true,
|
||||
},
|
||||
discordClientSecret: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true,
|
||||
},
|
||||
summaryProxy: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
|
@ -544,9 +499,6 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
defaultLightTheme: instance.defaultLightTheme,
|
||||
defaultDarkTheme: instance.defaultDarkTheme,
|
||||
enableEmail: instance.enableEmail,
|
||||
enableTwitterIntegration: instance.enableTwitterIntegration,
|
||||
enableGithubIntegration: instance.enableGithubIntegration,
|
||||
enableDiscordIntegration: instance.enableDiscordIntegration,
|
||||
enableServiceWorker: instance.enableServiceWorker,
|
||||
translatorAvailable:
|
||||
instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null,
|
||||
|
@ -573,12 +525,6 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
enableSensitiveMediaDetectionForVideos:
|
||||
instance.enableSensitiveMediaDetectionForVideos,
|
||||
proxyAccountId: instance.proxyAccountId,
|
||||
twitterConsumerKey: instance.twitterConsumerKey,
|
||||
twitterConsumerSecret: instance.twitterConsumerSecret,
|
||||
githubClientId: instance.githubClientId,
|
||||
githubClientSecret: instance.githubClientSecret,
|
||||
discordClientId: instance.discordClientId,
|
||||
discordClientSecret: instance.discordClientSecret,
|
||||
summalyProxy: instance.summalyProxy,
|
||||
email: instance.email,
|
||||
smtpSecure: instance.smtpSecure,
|
||||
|
|
|
@ -46,13 +46,6 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
};
|
||||
}
|
||||
|
||||
const maskedKeys = ["accessToken", "accessTokenSecret", "refreshToken"];
|
||||
Object.keys(profile.integrations).forEach((integration) => {
|
||||
maskedKeys.forEach(
|
||||
(key) => (profile.integrations[integration][key] = "<MASKED>"),
|
||||
);
|
||||
});
|
||||
|
||||
const signins = await Signins.findBy({ userId: user.id });
|
||||
|
||||
return {
|
||||
|
@ -67,7 +60,6 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
carefulBot: profile.carefulBot,
|
||||
injectFeaturedNote: profile.injectFeaturedNote,
|
||||
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
|
||||
integrations: profile.integrations,
|
||||
mutedWords: profile.mutedWords,
|
||||
mutedInstances: profile.mutedInstances,
|
||||
mutingNotificationTypes: profile.mutingNotificationTypes,
|
||||
|
|
|
@ -134,15 +134,6 @@ export const paramDef = {
|
|||
deeplIsPro: { type: "boolean" },
|
||||
libreTranslateApiUrl: { type: "string", nullable: true },
|
||||
libreTranslateApiKey: { type: "string", nullable: true },
|
||||
enableTwitterIntegration: { type: "boolean" },
|
||||
twitterConsumerKey: { type: "string", nullable: true },
|
||||
twitterConsumerSecret: { type: "string", nullable: true },
|
||||
enableGithubIntegration: { type: "boolean" },
|
||||
githubClientId: { type: "string", nullable: true },
|
||||
githubClientSecret: { type: "string", nullable: true },
|
||||
enableDiscordIntegration: { type: "boolean" },
|
||||
discordClientId: { type: "string", nullable: true },
|
||||
discordClientSecret: { type: "string", nullable: true },
|
||||
enableEmail: { type: "boolean" },
|
||||
email: { type: "string", nullable: true },
|
||||
smtpSecure: { type: "boolean" },
|
||||
|
@ -397,42 +388,6 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
set.summalyProxy = ps.summalyProxy;
|
||||
}
|
||||
|
||||
if (ps.enableTwitterIntegration !== undefined) {
|
||||
set.enableTwitterIntegration = ps.enableTwitterIntegration;
|
||||
}
|
||||
|
||||
if (ps.twitterConsumerKey !== undefined) {
|
||||
set.twitterConsumerKey = ps.twitterConsumerKey;
|
||||
}
|
||||
|
||||
if (ps.twitterConsumerSecret !== undefined) {
|
||||
set.twitterConsumerSecret = ps.twitterConsumerSecret;
|
||||
}
|
||||
|
||||
if (ps.enableGithubIntegration !== undefined) {
|
||||
set.enableGithubIntegration = ps.enableGithubIntegration;
|
||||
}
|
||||
|
||||
if (ps.githubClientId !== undefined) {
|
||||
set.githubClientId = ps.githubClientId;
|
||||
}
|
||||
|
||||
if (ps.githubClientSecret !== undefined) {
|
||||
set.githubClientSecret = ps.githubClientSecret;
|
||||
}
|
||||
|
||||
if (ps.enableDiscordIntegration !== undefined) {
|
||||
set.enableDiscordIntegration = ps.enableDiscordIntegration;
|
||||
}
|
||||
|
||||
if (ps.discordClientId !== undefined) {
|
||||
set.discordClientId = ps.discordClientId;
|
||||
}
|
||||
|
||||
if (ps.discordClientSecret !== undefined) {
|
||||
set.discordClientSecret = ps.discordClientSecret;
|
||||
}
|
||||
|
||||
if (ps.enableEmail !== undefined) {
|
||||
set.enableEmail = ps.enableEmail;
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ export const meta = {
|
|||
id: "4362f8dc-731f-4ad8-a694-be2a88922a24",
|
||||
},
|
||||
uriNull: {
|
||||
message: "User ActivityPup URI is null.",
|
||||
message: "User ActivityPub URI is null.",
|
||||
code: "URI_NULL",
|
||||
id: "bf326f31-d430-4f97-9933-5d61e4d48a23",
|
||||
},
|
||||
|
|
|
@ -54,12 +54,12 @@ export const meta = {
|
|||
id: "fcd2eef9-a9b2-4c4f-8624-038099e90aa5",
|
||||
},
|
||||
uriNull: {
|
||||
message: "User ActivityPup URI is null.",
|
||||
message: "User ActivityPub URI is null.",
|
||||
code: "URI_NULL",
|
||||
id: "bf326f31-d430-4f97-9933-5d61e4d48a23",
|
||||
},
|
||||
localUriNull: {
|
||||
message: "Local User ActivityPup URI is null.",
|
||||
message: "Local User ActivityPub URI is null.",
|
||||
code: "URI_NULL",
|
||||
id: "95ba11b9-90e8-43a5-ba16-7acc1ab32e71",
|
||||
},
|
||||
|
|
|
@ -268,21 +268,6 @@ export const meta = {
|
|||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
enableTwitterIntegration: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
enableGithubIntegration: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
enableDiscordIntegration: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
enableServiceWorker: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
|
@ -343,21 +328,6 @@ export const meta = {
|
|||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
twitter: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
github: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
discord: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
serviceWorker: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
|
@ -493,10 +463,6 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
})),
|
||||
enableEmail: instance.enableEmail,
|
||||
|
||||
enableTwitterIntegration: instance.enableTwitterIntegration,
|
||||
enableGithubIntegration: instance.enableGithubIntegration,
|
||||
enableDiscordIntegration: instance.enableDiscordIntegration,
|
||||
|
||||
enableServiceWorker: instance.enableServiceWorker,
|
||||
|
||||
translatorAvailable:
|
||||
|
@ -539,9 +505,6 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
hcaptcha: instance.enableHcaptcha,
|
||||
recaptcha: instance.enableRecaptcha,
|
||||
objectStorage: instance.useObjectStorage,
|
||||
twitter: instance.enableTwitterIntegration,
|
||||
github: instance.enableGithubIntegration,
|
||||
discord: instance.enableDiscordIntegration,
|
||||
serviceWorker: instance.enableServiceWorker,
|
||||
postEditing: true,
|
||||
postImports: instance.experimentalFeatures?.postImports || false,
|
||||
|
|
|
@ -21,9 +21,6 @@ import signup from "./private/signup.js";
|
|||
import signin from "./private/signin.js";
|
||||
import signupPending from "./private/signup-pending.js";
|
||||
import verifyEmail from "./private/verify-email.js";
|
||||
import discord from "./service/discord.js";
|
||||
import github from "./service/github.js";
|
||||
import twitter from "./service/twitter.js";
|
||||
import { koaBody } from "koa-body";
|
||||
import {
|
||||
convertId,
|
||||
|
@ -181,10 +178,6 @@ router.post("/signin", signin);
|
|||
router.post("/signup-pending", signupPending);
|
||||
router.post("/verify-email", verifyEmail);
|
||||
|
||||
router.use(discord.routes());
|
||||
router.use(github.routes());
|
||||
router.use(twitter.routes());
|
||||
|
||||
router.post("/miauth/:session/check", async (ctx) => {
|
||||
const token = await AccessTokens.findOneBy({
|
||||
session: ctx.params.session,
|
||||
|
|
|
@ -1,333 +0,0 @@
|
|||
import type Koa from "koa";
|
||||
import Router from "@koa/router";
|
||||
import { OAuth2 } from "oauth";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { IsNull } from "typeorm";
|
||||
import { getJson } from "@/misc/fetch.js";
|
||||
import config from "@/config/index.js";
|
||||
import { publishMainStream } from "@/services/stream.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import { Users, UserProfiles } from "@/models/index.js";
|
||||
import type { ILocalUser } from "@/models/entities/user.js";
|
||||
import { redisClient } from "../../../db/redis.js";
|
||||
import signin from "../common/signin.js";
|
||||
|
||||
function getUserToken(ctx: Koa.BaseContext): string | null {
|
||||
return ((ctx.headers["cookie"] || "").match(/igi=(\w+)/) || [null, null])[1];
|
||||
}
|
||||
|
||||
function compareOrigin(ctx: Koa.BaseContext): boolean {
|
||||
function normalizeUrl(url?: string): string {
|
||||
return url ? (url.endsWith("/") ? url.slice(0, url.length - 1) : url) : "";
|
||||
}
|
||||
|
||||
const referer = ctx.headers["referer"];
|
||||
|
||||
return normalizeUrl(referer) === normalizeUrl(config.url);
|
||||
}
|
||||
|
||||
// Init router
|
||||
const router = new Router();
|
||||
|
||||
router.get("/disconnect/discord", async (ctx) => {
|
||||
if (!compareOrigin(ctx)) {
|
||||
ctx.throw(400, "invalid origin");
|
||||
return;
|
||||
}
|
||||
|
||||
const userToken = getUserToken(ctx);
|
||||
if (!userToken) {
|
||||
ctx.throw(400, "signin required");
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await Users.findOneByOrFail({
|
||||
host: IsNull(),
|
||||
token: userToken,
|
||||
});
|
||||
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
|
||||
profile.integrations.discord = undefined;
|
||||
|
||||
await UserProfiles.update(user.id, {
|
||||
integrations: profile.integrations,
|
||||
});
|
||||
|
||||
ctx.body = "Discordの連携を解除しました :v:";
|
||||
|
||||
// Publish i updated event
|
||||
publishMainStream(
|
||||
user.id,
|
||||
"meUpdated",
|
||||
await Users.pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
async function getOAuth2() {
|
||||
const meta = await fetchMeta(true);
|
||||
|
||||
if (meta.enableDiscordIntegration) {
|
||||
return new OAuth2(
|
||||
meta.discordClientId!,
|
||||
meta.discordClientSecret!,
|
||||
"https://discord.com/",
|
||||
"api/oauth2/authorize",
|
||||
"api/oauth2/token",
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
router.get("/connect/discord", async (ctx) => {
|
||||
if (!compareOrigin(ctx)) {
|
||||
ctx.throw(400, "invalid origin");
|
||||
return;
|
||||
}
|
||||
|
||||
const userToken = getUserToken(ctx);
|
||||
if (!userToken) {
|
||||
ctx.throw(400, "signin required");
|
||||
return;
|
||||
}
|
||||
|
||||
const params = {
|
||||
redirect_uri: `${config.url}/api/dc/cb`,
|
||||
scope: ["identify"],
|
||||
state: uuid(),
|
||||
response_type: "code",
|
||||
};
|
||||
|
||||
redisClient.set(userToken, JSON.stringify(params));
|
||||
|
||||
const oauth2 = await getOAuth2();
|
||||
ctx.redirect(oauth2!.getAuthorizeUrl(params));
|
||||
});
|
||||
|
||||
router.get("/signin/discord", async (ctx) => {
|
||||
const sessid = uuid();
|
||||
|
||||
const params = {
|
||||
redirect_uri: `${config.url}/api/dc/cb`,
|
||||
scope: ["identify"],
|
||||
state: uuid(),
|
||||
response_type: "code",
|
||||
};
|
||||
|
||||
ctx.cookies.set("signin_with_discord_sid", sessid, {
|
||||
path: "/",
|
||||
secure: config.url.startsWith("https"),
|
||||
httpOnly: true,
|
||||
});
|
||||
|
||||
redisClient.set(sessid, JSON.stringify(params));
|
||||
|
||||
const oauth2 = await getOAuth2();
|
||||
ctx.redirect(oauth2!.getAuthorizeUrl(params));
|
||||
});
|
||||
|
||||
router.get("/dc/cb", async (ctx) => {
|
||||
const userToken = getUserToken(ctx);
|
||||
|
||||
const oauth2 = await getOAuth2();
|
||||
|
||||
if (!userToken) {
|
||||
const sessid = ctx.cookies.get("signin_with_discord_sid");
|
||||
|
||||
if (!sessid) {
|
||||
ctx.throw(400, "invalid session");
|
||||
return;
|
||||
}
|
||||
|
||||
const code = ctx.query.code;
|
||||
|
||||
if (!code || typeof code !== "string") {
|
||||
ctx.throw(400, "invalid session");
|
||||
return;
|
||||
}
|
||||
|
||||
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
|
||||
redisClient.get(sessid, async (_, state) => {
|
||||
res(JSON.parse(state));
|
||||
});
|
||||
});
|
||||
|
||||
if (ctx.query.state !== state) {
|
||||
ctx.throw(400, "invalid session");
|
||||
return;
|
||||
}
|
||||
|
||||
const { accessToken, refreshToken, expiresDate } = await new Promise<any>(
|
||||
(res, rej) =>
|
||||
oauth2!.getOAuthAccessToken(
|
||||
code,
|
||||
{
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri,
|
||||
},
|
||||
(err, accessToken, refreshToken, result) => {
|
||||
if (err) {
|
||||
rej(err);
|
||||
} else if (result.error) {
|
||||
rej(result.error);
|
||||
} else {
|
||||
res({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresDate: Date.now() + Number(result.expires_in) * 1000,
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const { id, username, discriminator } = (await getJson(
|
||||
"https://discord.com/api/users/@me",
|
||||
"*/*",
|
||||
10 * 1000,
|
||||
{
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
)) as Record<string, unknown>;
|
||||
|
||||
if (
|
||||
typeof id !== "string" ||
|
||||
typeof username !== "string" ||
|
||||
typeof discriminator !== "string"
|
||||
) {
|
||||
ctx.throw(400, "invalid session");
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = await UserProfiles.createQueryBuilder()
|
||||
.where("\"integrations\"->'discord'->>'id' = :id", { id: id })
|
||||
.andWhere('"userHost" IS NULL')
|
||||
.getOne();
|
||||
|
||||
if (profile == null) {
|
||||
ctx.throw(
|
||||
404,
|
||||
`@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await UserProfiles.update(profile.userId, {
|
||||
integrations: {
|
||||
...profile.integrations,
|
||||
discord: {
|
||||
id: id,
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
expiresDate: expiresDate,
|
||||
username: username,
|
||||
discriminator: discriminator,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
signin(
|
||||
ctx,
|
||||
(await Users.findOneBy({ id: profile.userId })) as ILocalUser,
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
const code = ctx.query.code;
|
||||
|
||||
if (!code || typeof code !== "string") {
|
||||
ctx.throw(400, "invalid session");
|
||||
return;
|
||||
}
|
||||
|
||||
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
|
||||
redisClient.get(userToken, async (_, state) => {
|
||||
res(JSON.parse(state));
|
||||
});
|
||||
});
|
||||
|
||||
if (ctx.query.state !== state) {
|
||||
ctx.throw(400, "invalid session");
|
||||
return;
|
||||
}
|
||||
|
||||
const { accessToken, refreshToken, expiresDate } = await new Promise<any>(
|
||||
(res, rej) =>
|
||||
oauth2!.getOAuthAccessToken(
|
||||
code,
|
||||
{
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri,
|
||||
},
|
||||
(err, accessToken, refreshToken, result) => {
|
||||
if (err) {
|
||||
rej(err);
|
||||
} else if (result.error) {
|
||||
rej(result.error);
|
||||
} else {
|
||||
res({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresDate: Date.now() + Number(result.expires_in) * 1000,
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const { id, username, discriminator } = (await getJson(
|
||||
"https://discord.com/api/users/@me",
|
||||
"*/*",
|
||||
10 * 1000,
|
||||
{
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
)) as Record<string, unknown>;
|
||||
if (
|
||||
typeof id !== "string" ||
|
||||
typeof username !== "string" ||
|
||||
typeof discriminator !== "string"
|
||||
) {
|
||||
ctx.throw(400, "invalid session");
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await Users.findOneByOrFail({
|
||||
host: IsNull(),
|
||||
token: userToken,
|
||||
});
|
||||
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
|
||||
await UserProfiles.update(user.id, {
|
||||
integrations: {
|
||||
...profile.integrations,
|
||||
discord: {
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
expiresDate: expiresDate,
|
||||
id: id,
|
||||
username: username,
|
||||
discriminator: discriminator,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ctx.body = `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました!`;
|
||||
|
||||
// Publish i updated event
|
||||
publishMainStream(
|
||||
user.id,
|
||||
"meUpdated",
|
||||
await Users.pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -1,296 +0,0 @@
|
|||
import type Koa from "koa";
|
||||
import Router from "@koa/router";
|
||||
import { OAuth2 } from "oauth";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { IsNull } from "typeorm";
|
||||
import { getJson } from "@/misc/fetch.js";
|
||||
import config from "@/config/index.js";
|
||||
import { publishMainStream } from "@/services/stream.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import { Users, UserProfiles } from "@/models/index.js";
|
||||
import type { ILocalUser } from "@/models/entities/user.js";
|
||||
import { redisClient } from "../../../db/redis.js";
|
||||
import signin from "../common/signin.js";
|
||||
|
||||
function getUserToken(ctx: Koa.BaseContext): string | null {
|
||||
return ((ctx.headers["cookie"] || "").match(/igi=(\w+)/) || [null, null])[1];
|
||||
}
|
||||
|
||||
function compareOrigin(ctx: Koa.BaseContext): boolean {
|
||||
function normalizeUrl(url?: string): string {
|
||||
return url ? (url.endsWith("/") ? url.slice(0, url.length - 1) : url) : "";
|
||||
}
|
||||
|
||||
const referer = ctx.headers["referer"];
|
||||
|
||||
return normalizeUrl(referer) === normalizeUrl(config.url);
|
||||
}
|
||||
|
||||
// Init router
|
||||
const router = new Router();
|
||||
|
||||
router.get("/disconnect/github", async (ctx) => {
|
||||
if (!compareOrigin(ctx)) {
|
||||
ctx.throw(400, "invalid origin");
|
||||
return;
|
||||
}
|
||||
|
||||
const userToken = getUserToken(ctx);
|
||||
if (!userToken) {
|
||||
ctx.throw(400, "signin required");
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await Users.findOneByOrFail({
|
||||
host: IsNull(),
|
||||
token: userToken,
|
||||
});
|
||||
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
|
||||
profile.integrations.github = undefined;
|
||||
|
||||
await UserProfiles.update(user.id, {
|
||||
integrations: profile.integrations,
|
||||
});
|
||||
|
||||
ctx.body = "GitHubの連携を解除しました :v:";
|
||||
|
||||
// Publish i updated event
|
||||
publishMainStream(
|
||||
user.id,
|
||||
"meUpdated",
|
||||
await Users.pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
async function getOath2() {
|
||||
const meta = await fetchMeta(true);
|
||||
|
||||
if (
|
||||
meta.enableGithubIntegration &&
|
||||
meta.githubClientId &&
|
||||
meta.githubClientSecret
|
||||
) {
|
||||
return new OAuth2(
|
||||
meta.githubClientId,
|
||||
meta.githubClientSecret,
|
||||
"https://github.com/",
|
||||
"login/oauth/authorize",
|
||||
"login/oauth/access_token",
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
router.get("/connect/github", async (ctx) => {
|
||||
if (!compareOrigin(ctx)) {
|
||||
ctx.throw(400, "invalid origin");
|
||||
return;
|
||||
}
|
||||
|
||||
const userToken = getUserToken(ctx);
|
||||
if (!userToken) {
|
||||
ctx.throw(400, "signin required");
|
||||
return;
|
||||
}
|
||||
|
||||
const params = {
|
||||
redirect_uri: `${config.url}/api/gh/cb`,
|
||||
scope: ["read:user"],
|
||||
state: uuid(),
|
||||
};
|
||||
|
||||
redisClient.set(userToken, JSON.stringify(params));
|
||||
|
||||
const oauth2 = await getOath2();
|
||||
ctx.redirect(oauth2!.getAuthorizeUrl(params));
|
||||
});
|
||||
|
||||
router.get("/signin/github", async (ctx) => {
|
||||
const sessid = uuid();
|
||||
|
||||
const params = {
|
||||
redirect_uri: `${config.url}/api/gh/cb`,
|
||||
scope: ["read:user"],
|
||||
state: uuid(),
|
||||
};
|
||||
|
||||
ctx.cookies.set("signin_with_github_sid", sessid, {
|
||||
path: "/",
|
||||
secure: config.url.startsWith("https"),
|
||||
httpOnly: true,
|
||||
});
|
||||
|
||||
redisClient.set(sessid, JSON.stringify(params));
|
||||
|
||||
const oauth2 = await getOath2();
|
||||
ctx.redirect(oauth2!.getAuthorizeUrl(params));
|
||||
});
|
||||
|
||||
router.get("/gh/cb", async (ctx) => {
|
||||
const userToken = getUserToken(ctx);
|
||||
|
||||
const oauth2 = await getOath2();
|
||||
|
||||
if (!userToken) {
|
||||
const sessid = ctx.cookies.get("signin_with_github_sid");
|
||||
|
||||
if (!sessid) {
|
||||
ctx.throw(400, "invalid session");
|
||||
return;
|
||||
}
|
||||
|
||||
const code = ctx.query.code;
|
||||
|
||||
if (!code || typeof code !== "string") {
|
||||
ctx.throw(400, "invalid session");
|
||||
return;
|
||||
}
|
||||
|
||||
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
|
||||
redisClient.get(sessid, async (_, state) => {
|
||||
res(JSON.parse(state));
|
||||
});
|
||||
});
|
||||
|
||||
if (ctx.query.state !== state) {
|
||||
ctx.throw(400, "invalid session");
|
||||
return;
|
||||
}
|
||||
|
||||
const { accessToken } = await new Promise<any>((res, rej) =>
|
||||
oauth2!.getOAuthAccessToken(
|
||||
code,
|
||||
{
|
||||
redirect_uri,
|
||||
},
|
||||
(err, accessToken, refresh, result) => {
|
||||
if (err) {
|
||||
rej(err);
|
||||
} else if (result.error) {
|
||||
rej(result.error);
|
||||
} else {
|
||||
res({ accessToken });
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const { login, id } = (await getJson(
|
||||
"https://api.github.com/user",
|
||||
"application/vnd.github.v3+json",
|
||||
10 * 1000,
|
||||
{
|
||||
Authorization: `bearer ${accessToken}`,
|
||||
},
|
||||
)) as Record<string, unknown>;
|
||||
if (typeof login !== "string" || typeof id !== "string") {
|
||||
ctx.throw(400, "invalid session");
|
||||
return;
|
||||
}
|
||||
|
||||
const link = await UserProfiles.createQueryBuilder()
|
||||
.where("\"integrations\"->'github'->>'id' = :id", { id: id })
|
||||
.andWhere('"userHost" IS NULL')
|
||||
.getOne();
|
||||
|
||||
if (link == null) {
|
||||
ctx.throw(
|
||||
404,
|
||||
`@${login}と連携しているMisskeyアカウントはありませんでした...`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
signin(
|
||||
ctx,
|
||||
(await Users.findOneBy({ id: link.userId })) as ILocalUser,
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
const code = ctx.query.code;
|
||||
|
||||
if (!code || typeof code !== "string") {
|
||||
ctx.throw(400, "invalid session");
|
||||
return;
|
||||
}
|
||||
|
||||
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
|
||||
redisClient.get(userToken, async (_, state) => {
|
||||
res(JSON.parse(state));
|
||||
});
|
||||
});
|
||||
|
||||
if (ctx.query.state !== state) {
|
||||
ctx.throw(400, "invalid session");
|
||||
return;
|
||||
}
|
||||
|
||||
const { accessToken } = await new Promise<any>((res, rej) =>
|
||||
oauth2!.getOAuthAccessToken(
|
||||
code,
|
||||
{ redirect_uri },
|
||||
(err, accessToken, refresh, result) => {
|
||||
if (err) {
|
||||
rej(err);
|
||||
} else if (result.error) {
|
||||
rej(result.error);
|
||||
} else {
|
||||
res({ accessToken });
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const { login, id } = (await getJson(
|
||||
"https://api.github.com/user",
|
||||
"application/vnd.github.v3+json",
|
||||
10 * 1000,
|
||||
{
|
||||
Authorization: `bearer ${accessToken}`,
|
||||
},
|
||||
)) as Record<string, unknown>;
|
||||
|
||||
if (typeof login !== "string" || typeof id !== "string") {
|
||||
ctx.throw(400, "invalid session");
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await Users.findOneByOrFail({
|
||||
host: IsNull(),
|
||||
token: userToken,
|
||||
});
|
||||
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
|
||||
await UserProfiles.update(user.id, {
|
||||
integrations: {
|
||||
...profile.integrations,
|
||||
github: {
|
||||
accessToken: accessToken,
|
||||
id: id,
|
||||
login: login,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`;
|
||||
|
||||
// Publish i updated event
|
||||
publishMainStream(
|
||||
user.id,
|
||||
"meUpdated",
|
||||
await Users.pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -1,226 +0,0 @@
|
|||
import type Koa from "koa";
|
||||
import Router from "@koa/router";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import autwh from "autwh";
|
||||
import { IsNull } from "typeorm";
|
||||
import { publishMainStream } from "@/services/stream.js";
|
||||
import config from "@/config/index.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import { Users, UserProfiles } from "@/models/index.js";
|
||||
import type { ILocalUser } from "@/models/entities/user.js";
|
||||
import signin from "../common/signin.js";
|
||||
import { redisClient } from "../../../db/redis.js";
|
||||
|
||||
function getUserToken(ctx: Koa.BaseContext): string | null {
|
||||
return ((ctx.headers["cookie"] || "").match(/igi=(\w+)/) || [null, null])[1];
|
||||
}
|
||||
|
||||
function compareOrigin(ctx: Koa.BaseContext): boolean {
|
||||
function normalizeUrl(url?: string): string {
|
||||
return url == null
|
||||
? ""
|
||||
: url.endsWith("/")
|
||||
? url.substr(0, url.length - 1)
|
||||
: url;
|
||||
}
|
||||
|
||||
const referer = ctx.headers["referer"];
|
||||
|
||||
return normalizeUrl(referer) === normalizeUrl(config.url);
|
||||
}
|
||||
|
||||
// Init router
|
||||
const router = new Router();
|
||||
|
||||
router.get("/disconnect/twitter", async (ctx) => {
|
||||
if (!compareOrigin(ctx)) {
|
||||
ctx.throw(400, "invalid origin");
|
||||
return;
|
||||
}
|
||||
|
||||
const userToken = getUserToken(ctx);
|
||||
if (userToken == null) {
|
||||
ctx.throw(400, "signin required");
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await Users.findOneByOrFail({
|
||||
host: IsNull(),
|
||||
token: userToken,
|
||||
});
|
||||
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
|
||||
profile.integrations.twitter = undefined;
|
||||
|
||||
await UserProfiles.update(user.id, {
|
||||
integrations: profile.integrations,
|
||||
});
|
||||
|
||||
ctx.body = "Twitterの連携を解除しました :v:";
|
||||
|
||||
// Publish i updated event
|
||||
publishMainStream(
|
||||
user.id,
|
||||
"meUpdated",
|
||||
await Users.pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
async function getTwAuth() {
|
||||
const meta = await fetchMeta(true);
|
||||
|
||||
if (
|
||||
meta.enableTwitterIntegration &&
|
||||
meta.twitterConsumerKey &&
|
||||
meta.twitterConsumerSecret
|
||||
) {
|
||||
return autwh({
|
||||
consumerKey: meta.twitterConsumerKey,
|
||||
consumerSecret: meta.twitterConsumerSecret,
|
||||
callbackUrl: `${config.url}/api/tw/cb`,
|
||||
});
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
router.get("/connect/twitter", async (ctx) => {
|
||||
if (!compareOrigin(ctx)) {
|
||||
ctx.throw(400, "invalid origin");
|
||||
return;
|
||||
}
|
||||
|
||||
const userToken = getUserToken(ctx);
|
||||
if (userToken == null) {
|
||||
ctx.throw(400, "signin required");
|
||||
return;
|
||||
}
|
||||
|
||||
const twAuth = await getTwAuth();
|
||||
const twCtx = await twAuth!.begin();
|
||||
redisClient.set(userToken, JSON.stringify(twCtx));
|
||||
ctx.redirect(twCtx.url);
|
||||
});
|
||||
|
||||
router.get("/signin/twitter", async (ctx) => {
|
||||
const twAuth = await getTwAuth();
|
||||
const twCtx = await twAuth!.begin();
|
||||
|
||||
const sessid = uuid();
|
||||
|
||||
redisClient.set(sessid, JSON.stringify(twCtx));
|
||||
|
||||
ctx.cookies.set("signin_with_twitter_sid", sessid, {
|
||||
path: "/",
|
||||
secure: config.url.startsWith("https"),
|
||||
httpOnly: true,
|
||||
});
|
||||
|
||||
ctx.redirect(twCtx.url);
|
||||
});
|
||||
|
||||
router.get("/tw/cb", async (ctx) => {
|
||||
const userToken = getUserToken(ctx);
|
||||
|
||||
const twAuth = await getTwAuth();
|
||||
|
||||
if (userToken == null) {
|
||||
const sessid = ctx.cookies.get("signin_with_twitter_sid");
|
||||
|
||||
if (sessid == null) {
|
||||
ctx.throw(400, "invalid session");
|
||||
return;
|
||||
}
|
||||
|
||||
const get = new Promise<any>((res, rej) => {
|
||||
redisClient.get(sessid, async (_, twCtx) => {
|
||||
res(twCtx);
|
||||
});
|
||||
});
|
||||
|
||||
const twCtx = await get;
|
||||
|
||||
const verifier = ctx.query.oauth_verifier;
|
||||
if (!verifier || typeof verifier !== "string") {
|
||||
ctx.throw(400, "invalid session");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await twAuth!.done(JSON.parse(twCtx), verifier);
|
||||
|
||||
const link = await UserProfiles.createQueryBuilder()
|
||||
.where("\"integrations\"->'twitter'->>'userId' = :id", {
|
||||
id: result.userId,
|
||||
})
|
||||
.andWhere('"userHost" IS NULL')
|
||||
.getOne();
|
||||
|
||||
if (link == null) {
|
||||
ctx.throw(
|
||||
404,
|
||||
`@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
signin(
|
||||
ctx,
|
||||
(await Users.findOneBy({ id: link.userId })) as ILocalUser,
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
const verifier = ctx.query.oauth_verifier;
|
||||
|
||||
if (!verifier || typeof verifier !== "string") {
|
||||
ctx.throw(400, "invalid session");
|
||||
return;
|
||||
}
|
||||
|
||||
const get = new Promise<any>((res, rej) => {
|
||||
redisClient.get(userToken, async (_, twCtx) => {
|
||||
res(twCtx);
|
||||
});
|
||||
});
|
||||
|
||||
const twCtx = await get;
|
||||
|
||||
const result = await twAuth!.done(JSON.parse(twCtx), verifier);
|
||||
|
||||
const user = await Users.findOneByOrFail({
|
||||
host: IsNull(),
|
||||
token: userToken,
|
||||
});
|
||||
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
|
||||
await UserProfiles.update(user.id, {
|
||||
integrations: {
|
||||
...profile.integrations,
|
||||
twitter: {
|
||||
accessToken: result.accessToken,
|
||||
accessTokenSecret: result.accessTokenSecret,
|
||||
userId: result.userId,
|
||||
screenName: result.screenName,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`;
|
||||
|
||||
// Publish i updated event
|
||||
publishMainStream(
|
||||
user.id,
|
||||
"meUpdated",
|
||||
await Users.pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -68,7 +68,7 @@ export default class extends Channel {
|
|||
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
||||
if (
|
||||
this.userProfile &&
|
||||
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
|
||||
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords))
|
||||
)
|
||||
return;
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ export default class extends Channel {
|
|||
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
||||
if (
|
||||
this.userProfile &&
|
||||
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
|
||||
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords))
|
||||
)
|
||||
return;
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ export default class extends Channel {
|
|||
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
||||
if (
|
||||
this.userProfile &&
|
||||
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
|
||||
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords))
|
||||
)
|
||||
return;
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ export default class extends Channel {
|
|||
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
||||
if (
|
||||
this.userProfile &&
|
||||
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
|
||||
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords))
|
||||
)
|
||||
return;
|
||||
|
||||
|
|
|
@ -82,7 +82,7 @@ export default class extends Channel {
|
|||
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
||||
if (
|
||||
this.userProfile &&
|
||||
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
|
||||
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords))
|
||||
)
|
||||
return;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as mfm from "mfm-js";
|
||||
import es from "../../db/elasticsearch.js";
|
||||
import sonic from "../../db/sonic.js";
|
||||
import es from "@/db/elasticsearch.js";
|
||||
import sonic from "@/db/sonic.js";
|
||||
import {
|
||||
publishMainStream,
|
||||
publishNotesStream,
|
||||
|
@ -386,22 +386,18 @@ export default async (
|
|||
|
||||
// Word mute
|
||||
if (!scyllaClient) {
|
||||
mutedWordsCache
|
||||
.fetch(null, () =>
|
||||
UserProfiles.find({
|
||||
where: {
|
||||
enableWordMute: true,
|
||||
},
|
||||
select: ["userId", "mutedWords"],
|
||||
}),
|
||||
)
|
||||
.then((us) => {
|
||||
for (const u of us) {
|
||||
const shouldMute = getWordHardMute(
|
||||
data,
|
||||
{ id: u.userId },
|
||||
u.mutedWords,
|
||||
);
|
||||
mutedWordsCache
|
||||
.fetch(null, () =>
|
||||
UserProfiles.find({
|
||||
where: {
|
||||
enableWordMute: true,
|
||||
},
|
||||
select: ["userId", "mutedWords"],
|
||||
}),
|
||||
)
|
||||
.then((us) => {
|
||||
for (const u of us) {
|
||||
getWordHardMute(data, u.userId, u.mutedWords).then((shouldMute) => {
|
||||
if (shouldMute) {
|
||||
MutedNotes.insert({
|
||||
id: genId(),
|
||||
|
@ -410,8 +406,8 @@ export default async (
|
|||
reason: "word",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}});
|
||||
}
|
||||
|
||||
// Antenna
|
||||
|
|
|
@ -273,7 +273,6 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, onMounted, ref } from "vue";
|
||||
import * as mfm from "mfm-js";
|
||||
import type { Ref } from "vue";
|
||||
import type * as firefish from "firefish-js";
|
||||
import MkSubNoteContent from "./MkSubNoteContent.vue";
|
||||
|
@ -360,7 +359,7 @@ const isDeleted = ref(false);
|
|||
const muted = ref(
|
||||
getWordSoftMute(
|
||||
note.value,
|
||||
$i,
|
||||
$i.id,
|
||||
defaultStore.state.mutedWords,
|
||||
defaultStore.state.mutedLangs,
|
||||
),
|
||||
|
|
|
@ -36,30 +36,47 @@
|
|||
<MkTab v-model="tab" :style="'underline'" @update:modelValue="loadTab">
|
||||
<option value="replies">
|
||||
<!-- <i class="ph-arrow-u-up-left ph-bold ph-lg"></i> -->
|
||||
<span v-if="note.repliesCount > 0" class="count">{{
|
||||
note.repliesCount
|
||||
}}</span>
|
||||
{{ i18n.ts._notification._types.reply }}
|
||||
{{
|
||||
wordWithCount(
|
||||
note.repliesCount,
|
||||
i18n.ts.reply,
|
||||
i18n.ts.replies,
|
||||
)
|
||||
}}
|
||||
</option>
|
||||
<option v-if="note.renoteCount > 0" value="renotes">
|
||||
<!-- <i class="ph-rocket-launch ph-bold ph-lg"></i> -->
|
||||
<span class="count">{{ note.renoteCount }}</span>
|
||||
{{ i18n.ts._notification._types.renote }}
|
||||
{{
|
||||
wordWithCount(
|
||||
note.renoteCount,
|
||||
i18n.ts.renote,
|
||||
i18n.ts.renotes,
|
||||
)
|
||||
}}
|
||||
</option>
|
||||
<option v-if="reactionsCount > 0" value="reactions">
|
||||
<!-- <i class="ph-smiley ph-bold ph-lg"></i> -->
|
||||
<span class="count">{{ reactionsCount }}</span>
|
||||
{{ i18n.ts.reaction }}
|
||||
{{
|
||||
wordWithCount(
|
||||
reactionsCount,
|
||||
i18n.ts.reaction,
|
||||
i18n.ts.reactions,
|
||||
)
|
||||
}}
|
||||
</option>
|
||||
<option v-if="directQuotes?.length > 0" value="quotes">
|
||||
<!-- <i class="ph-quotes ph-bold ph-lg"></i> -->
|
||||
<span class="count">{{ directQuotes.length }}</span>
|
||||
{{ i18n.ts._notification._types.quote }}
|
||||
{{
|
||||
wordWithCount(
|
||||
directQuotes.length,
|
||||
i18n.ts.quote,
|
||||
i18n.ts.quotes,
|
||||
)
|
||||
}}
|
||||
</option>
|
||||
<option v-if="clips?.length > 0" value="clips">
|
||||
<!-- <i class="ph-paperclip ph-bold ph-lg"></i> -->
|
||||
<span class="count">{{ clips.length }}</span>
|
||||
{{ i18n.ts.clips }}
|
||||
{{ wordWithCount(clips.length, i18n.ts.clip, i18n.ts.clips) }}
|
||||
</option>
|
||||
</MkTab>
|
||||
|
||||
|
@ -191,6 +208,11 @@ const softMuteReasonI18nSrc = (what?: string) => {
|
|||
return i18n.ts.userSaysSomething;
|
||||
};
|
||||
|
||||
const wordWithCount = (count: number, singular: string, plural: string) => {
|
||||
if (count === 0) return plural;
|
||||
return `${count} ${count === 1 ? singular : plural}`;
|
||||
};
|
||||
|
||||
// plugin
|
||||
if (noteViewInterruptors.length > 0) {
|
||||
onMounted(async () => {
|
||||
|
@ -212,7 +234,7 @@ const isDeleted = ref(false);
|
|||
const muted = ref(
|
||||
getWordSoftMute(
|
||||
note.value,
|
||||
$i,
|
||||
$i.id,
|
||||
defaultStore.state.mutedWords,
|
||||
defaultStore.state.mutedLangs,
|
||||
),
|
||||
|
|
|
@ -268,7 +268,7 @@ const isDeleted = ref(false);
|
|||
const muted = ref(
|
||||
getWordSoftMute(
|
||||
note.value,
|
||||
$i,
|
||||
$i.id,
|
||||
defaultStore.state.mutedWords,
|
||||
defaultStore.state.mutedLangs,
|
||||
),
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
:to="notePage(notification.note)"
|
||||
:title="getNoteSummary(notification.note)"
|
||||
>
|
||||
<span>{{ i18n.ts._notification.reacted }}</span>
|
||||
<span>{{ i18n.ts._notification.reacted }}: </span>
|
||||
<i class="ph-quotes ph-fill ph-lg"></i>
|
||||
<Mfm
|
||||
:text="getNoteSummary(notification.note)"
|
||||
|
@ -130,7 +130,7 @@
|
|||
:to="notePage(notification.note)"
|
||||
:title="getNoteSummary(notification.note.renote)"
|
||||
>
|
||||
<span>{{ i18n.ts._notification.renoted }}</span>
|
||||
<span>{{ i18n.ts._notification.renoted }}: </span>
|
||||
<i class="ph-quotes ph-fill ph-lg"></i>
|
||||
<Mfm
|
||||
:text="getNoteSummary(notification.note.renote)"
|
||||
|
@ -185,7 +185,7 @@
|
|||
:to="notePage(notification.note)"
|
||||
:title="getNoteSummary(notification.note)"
|
||||
>
|
||||
<span>{{ i18n.ts._notification.voted }}</span>
|
||||
<span>{{ i18n.ts._notification.voted }}: </span>
|
||||
<i class="ph-quotes ph-fill ph-lg"></i>
|
||||
<Mfm
|
||||
:text="getNoteSummary(notification.note)"
|
||||
|
@ -518,9 +518,6 @@ useTooltip(reactionRef, (showing) => {
|
|||
|
||||
> span:first-child {
|
||||
opacity: 0.7;
|
||||
&::after {
|
||||
content: ": ";
|
||||
}
|
||||
}
|
||||
|
||||
> i {
|
||||
|
|
|
@ -47,11 +47,11 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const note = ref<firefish.entities.Note>();
|
||||
const tab = ref<string>();
|
||||
const tab = ref<string | null>(null);
|
||||
const reactions = ref<string[]>();
|
||||
const users = ref();
|
||||
|
||||
watch(tab, async () => {
|
||||
async function updateUsers(): void {
|
||||
const res = await os.api("notes/reactions", {
|
||||
noteId: props.noteId,
|
||||
type: tab.value,
|
||||
|
@ -59,15 +59,17 @@ watch(tab, async () => {
|
|||
});
|
||||
|
||||
users.value = res.map((x) => x.user);
|
||||
});
|
||||
}
|
||||
|
||||
watch(tab, updateUsers);
|
||||
|
||||
onMounted(() => {
|
||||
os.api("notes/show", {
|
||||
noteId: props.noteId,
|
||||
}).then((res) => {
|
||||
}).then(async (res) => {
|
||||
reactions.value = Object.keys(res.reactions);
|
||||
tab.value = reactions.value[0];
|
||||
note.value = res;
|
||||
await updateUsers();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -124,38 +124,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="social _section">
|
||||
<a
|
||||
v-if="meta && meta.enableTwitterIntegration"
|
||||
class="_borderButton _gap"
|
||||
:href="`${apiUrl}/signin/twitter`"
|
||||
><i
|
||||
class="ph-twitter-logo ph-bold ph-lg"
|
||||
style="margin-right: 4px"
|
||||
></i
|
||||
>{{ i18n.t("signinWith", { x: "Twitter" }) }}</a
|
||||
>
|
||||
<a
|
||||
v-if="meta && meta.enableGithubIntegration"
|
||||
class="_borderButton _gap"
|
||||
:href="`${apiUrl}/signin/github`"
|
||||
><i
|
||||
class="ph-github-logo ph-bold ph-lg"
|
||||
style="margin-right: 4px"
|
||||
></i
|
||||
>{{ i18n.t("signinWith", { x: "GitHub" }) }}</a
|
||||
>
|
||||
<a
|
||||
v-if="meta && meta.enableDiscordIntegration"
|
||||
class="_borderButton _gap"
|
||||
:href="`${apiUrl}/signin/discord`"
|
||||
><i
|
||||
class="ph-discord-logo ph-bold ph-lg"
|
||||
style="margin-right: 4px"
|
||||
></i
|
||||
>{{ i18n.t("signinWith", { x: "Discord" }) }}</a
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
|
@ -169,7 +137,6 @@ import { apiUrl, host as configHost } from "@/config";
|
|||
import { byteify, hexify } from "@/scripts/2fa";
|
||||
import * as os from "@/os";
|
||||
import { login } from "@/account";
|
||||
import { instance } from "@/instance";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
const signing = ref(false);
|
||||
|
@ -184,8 +151,6 @@ const queryingKey = ref(false);
|
|||
const hCaptchaResponse = ref(null);
|
||||
const reCaptchaResponse = ref(null);
|
||||
|
||||
const meta = computed(() => instance);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: "login", v: any): void;
|
||||
}>();
|
||||
|
|
|
@ -267,14 +267,6 @@ const menuDef = computed(() => [
|
|||
to: "/admin/relays",
|
||||
active: currentPage.value?.route.name === "relays",
|
||||
},
|
||||
{
|
||||
icon: "ph-plug ph-bold ph-lg",
|
||||
text: i18n.ts.integration,
|
||||
to: "/admin/integrations",
|
||||
active:
|
||||
currentPage.value?.route.name ===
|
||||
"integrations",
|
||||
},
|
||||
{
|
||||
icon: "ph-prohibit ph-bold ph-lg",
|
||||
text: i18n.ts.instanceBlocking,
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
<template>
|
||||
<FormSuspense :p="init">
|
||||
<div class="_formRoot">
|
||||
<FormSwitch v-model="enableDiscordIntegration" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<template v-if="enableDiscordIntegration">
|
||||
<FormInfo class="_formBlock"
|
||||
>Callback URL: {{ `${uri}/api/dc/cb` }}</FormInfo
|
||||
>
|
||||
|
||||
<FormInput v-model="discordClientId" class="_formBlock">
|
||||
<template #prefix
|
||||
><i class="ph-key ph-bold ph-lg"></i
|
||||
></template>
|
||||
<template #label>Client ID</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="discordClientSecret" class="_formBlock">
|
||||
<template #prefix
|
||||
><i class="ph-key ph-bold ph-lg"></i
|
||||
></template>
|
||||
<template #label>Client Secret</template>
|
||||
</FormInput>
|
||||
</template>
|
||||
|
||||
<FormButton primary class="_formBlock" @click="save"
|
||||
><i class="ph-floppy-disk-back ph-bold ph-lg"></i>
|
||||
{{ i18n.ts.save }}</FormButton
|
||||
>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
|
||||
import FormSwitch from "@/components/form/switch.vue";
|
||||
import FormInput from "@/components/form/input.vue";
|
||||
import FormButton from "@/components/MkButton.vue";
|
||||
import FormInfo from "@/components/MkInfo.vue";
|
||||
import FormSuspense from "@/components/form/suspense.vue";
|
||||
import * as os from "@/os";
|
||||
import { fetchInstance } from "@/instance";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
const uri = ref("");
|
||||
const enableDiscordIntegration = ref(false);
|
||||
const discordClientId = ref(null);
|
||||
const discordClientSecret = ref(null);
|
||||
|
||||
async function init() {
|
||||
const meta = await os.api("admin/meta");
|
||||
uri.value = meta.uri;
|
||||
enableDiscordIntegration.value = meta.enableDiscordIntegration;
|
||||
discordClientId.value = meta.discordClientId;
|
||||
discordClientSecret.value = meta.discordClientSecret;
|
||||
}
|
||||
|
||||
function save() {
|
||||
os.apiWithDialog("admin/update-meta", {
|
||||
enableDiscordIntegration: enableDiscordIntegration.value,
|
||||
discordClientId: discordClientId.value,
|
||||
discordClientSecret: discordClientSecret.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
</script>
|
|
@ -1,70 +0,0 @@
|
|||
<template>
|
||||
<FormSuspense :p="init">
|
||||
<div class="_formRoot">
|
||||
<FormSwitch v-model="enableGithubIntegration" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<template v-if="enableGithubIntegration">
|
||||
<FormInfo class="_formBlock"
|
||||
>Callback URL: {{ `${uri}/api/gh/cb` }}</FormInfo
|
||||
>
|
||||
|
||||
<FormInput v-model="githubClientId" class="_formBlock">
|
||||
<template #prefix
|
||||
><i class="ph-key ph-bold ph-lg"></i
|
||||
></template>
|
||||
<template #label>Client ID</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="githubClientSecret" class="_formBlock">
|
||||
<template #prefix
|
||||
><i class="ph-key ph-bold ph-lg"></i
|
||||
></template>
|
||||
<template #label>Client Secret</template>
|
||||
</FormInput>
|
||||
</template>
|
||||
|
||||
<FormButton primary class="_formBlock" @click="save"
|
||||
><i class="ph-floppy-disk-back ph-bold ph-lg"></i>
|
||||
{{ i18n.ts.save }}</FormButton
|
||||
>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
|
||||
import FormSwitch from "@/components/form/switch.vue";
|
||||
import FormInput from "@/components/form/input.vue";
|
||||
import FormButton from "@/components/MkButton.vue";
|
||||
import FormInfo from "@/components/MkInfo.vue";
|
||||
import FormSuspense from "@/components/form/suspense.vue";
|
||||
import * as os from "@/os";
|
||||
import { fetchInstance } from "@/instance";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
const uri = ref("");
|
||||
const enableGithubIntegration = ref(false);
|
||||
const githubClientId = ref(null);
|
||||
const githubClientSecret = ref(null);
|
||||
|
||||
async function init() {
|
||||
const meta = await os.api("admin/meta");
|
||||
uri.value = meta.uri;
|
||||
enableGithubIntegration.value = meta.enableGithubIntegration;
|
||||
githubClientId.value = meta.githubClientId;
|
||||
githubClientSecret.value = meta.githubClientSecret;
|
||||
}
|
||||
|
||||
function save() {
|
||||
os.apiWithDialog("admin/update-meta", {
|
||||
enableGithubIntegration: enableGithubIntegration.value,
|
||||
githubClientId: githubClientId.value,
|
||||
githubClientSecret: githubClientSecret.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
</script>
|
|
@ -1,68 +0,0 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header
|
||||
><MkPageHeader
|
||||
:actions="headerActions"
|
||||
:tabs="headerTabs"
|
||||
:display-back-button="true"
|
||||
/></template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<FormFolder class="_formBlock">
|
||||
<template #icon
|
||||
><i class="ph-github-logo ph-bold ph-lg"></i
|
||||
></template>
|
||||
<template #label>GitHub</template>
|
||||
<template #suffix>{{
|
||||
enableGithubIntegration
|
||||
? i18n.ts.enabled
|
||||
: i18n.ts.disabled
|
||||
}}</template>
|
||||
<XGithub />
|
||||
</FormFolder>
|
||||
<FormFolder class="_formBlock">
|
||||
<template #icon
|
||||
><i class="ph-discord-logo ph-bold ph-lg"></i
|
||||
></template>
|
||||
<template #label>Discord</template>
|
||||
<template #suffix>{{
|
||||
enableDiscordIntegration
|
||||
? i18n.ts.enabled
|
||||
: i18n.ts.disabled
|
||||
}}</template>
|
||||
<XDiscord />
|
||||
</FormFolder>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import XGithub from "./integrations.github.vue";
|
||||
import XDiscord from "./integrations.discord.vue";
|
||||
import FormSuspense from "@/components/form/suspense.vue";
|
||||
import FormFolder from "@/components/form/folder.vue";
|
||||
import * as os from "@/os";
|
||||
import { i18n } from "@/i18n";
|
||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
|
||||
const enableGithubIntegration = ref(false);
|
||||
const enableDiscordIntegration = ref(false);
|
||||
|
||||
async function init() {
|
||||
const meta = await os.api("admin/meta");
|
||||
enableGithubIntegration.value = meta.enableGithubIntegration;
|
||||
enableDiscordIntegration.value = meta.enableDiscordIntegration;
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.integration,
|
||||
icon: "ph-plug ph-bold ph-lg",
|
||||
});
|
||||
</script>
|
|
@ -116,12 +116,6 @@ const menuDef = computed(() => [
|
|||
to: "/settings/email",
|
||||
active: currentPage.value?.route.name === "email",
|
||||
},
|
||||
{
|
||||
icon: "ph-share-network ph-bold ph-lg",
|
||||
text: i18n.ts.integration,
|
||||
to: "/settings/integration",
|
||||
active: currentPage.value?.route.name === "integration",
|
||||
},
|
||||
{
|
||||
icon: "ph-lock ph-bold ph-lg",
|
||||
text: i18n.ts.security,
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormSection v-if="instance.enableDiscordIntegration">
|
||||
<template #label
|
||||
><i class="ph-discord-logo ph-bold ph-lg"></i> Discord</template
|
||||
>
|
||||
<p v-if="integrations.discord">
|
||||
{{ i18n.ts.connectedTo }}:
|
||||
<a
|
||||
:href="`https://discord.com/users/${integrations.discord.id}`"
|
||||
rel="nofollow noopener"
|
||||
target="_blank"
|
||||
>@{{ integrations.discord.username }}#{{
|
||||
integrations.discord.discriminator
|
||||
}}</a
|
||||
>
|
||||
</p>
|
||||
<MkButton
|
||||
v-if="integrations.discord"
|
||||
danger
|
||||
@click="disconnectDiscord"
|
||||
>{{ i18n.ts.disconnectService }}</MkButton
|
||||
>
|
||||
<MkButton v-else primary @click="connectDiscord">{{
|
||||
i18n.ts.connectService
|
||||
}}</MkButton>
|
||||
</FormSection>
|
||||
|
||||
<FormSection v-if="instance.enableGithubIntegration">
|
||||
<template #label
|
||||
><i class="ph-github-logo ph-bold ph-lg"></i> GitHub</template
|
||||
>
|
||||
<p v-if="integrations.github">
|
||||
{{ i18n.ts.connectedTo }}:
|
||||
<a
|
||||
:href="`https://github.com/${integrations.github.login}`"
|
||||
rel="nofollow noopener"
|
||||
target="_blank"
|
||||
>@{{ integrations.github.login }}</a
|
||||
>
|
||||
</p>
|
||||
<MkButton
|
||||
v-if="integrations.github"
|
||||
danger
|
||||
@click="disconnectGithub"
|
||||
>{{ i18n.ts.disconnectService }}</MkButton
|
||||
>
|
||||
<MkButton v-else primary @click="connectGithub">{{
|
||||
i18n.ts.connectService
|
||||
}}</MkButton>
|
||||
</FormSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { apiUrl } from "@/config";
|
||||
import FormSection from "@/components/form/section.vue";
|
||||
import MkButton from "@/components/MkButton.vue";
|
||||
import { $i } from "@/account";
|
||||
import { instance } from "@/instance";
|
||||
import { i18n } from "@/i18n";
|
||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
|
||||
const twitterForm = ref<Window | null>(null);
|
||||
const discordForm = ref<Window | null>(null);
|
||||
const githubForm = ref<Window | null>(null);
|
||||
|
||||
const integrations = computed(() => $i!.integrations);
|
||||
|
||||
function openWindow(service: string, type: string) {
|
||||
return window.open(
|
||||
`${apiUrl}/${type}/${service}`,
|
||||
`${service}_${type}_window`,
|
||||
"height=570, width=520",
|
||||
);
|
||||
}
|
||||
|
||||
function connectDiscord() {
|
||||
discordForm.value = openWindow("discord", "connect");
|
||||
}
|
||||
|
||||
function disconnectDiscord() {
|
||||
openWindow("discord", "disconnect");
|
||||
}
|
||||
|
||||
function connectGithub() {
|
||||
githubForm.value = openWindow("github", "connect");
|
||||
}
|
||||
|
||||
function disconnectGithub() {
|
||||
openWindow("github", "disconnect");
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.cookie =
|
||||
`igi=${$i!.token}; path=/;` +
|
||||
" max-age=31536000;" +
|
||||
(document.location.protocol.startsWith("https") ? " secure" : "");
|
||||
|
||||
watch(integrations, () => {
|
||||
if (integrations.value.twitter) {
|
||||
if (twitterForm.value) twitterForm.value.close();
|
||||
}
|
||||
if (integrations.value.discord) {
|
||||
if (discordForm.value) discordForm.value.close();
|
||||
}
|
||||
if (integrations.value.github) {
|
||||
if (githubForm.value) githubForm.value.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.integration,
|
||||
icon: "ph-share-network ph-bold ph-lg",
|
||||
});
|
||||
</script>
|
|
@ -103,11 +103,6 @@ export const routes = [
|
|||
name: "email",
|
||||
component: page(() => import("./pages/settings/email.vue")),
|
||||
},
|
||||
{
|
||||
path: "/integration",
|
||||
name: "integration",
|
||||
component: page(() => import("./pages/settings/integration.vue")),
|
||||
},
|
||||
{
|
||||
path: "/security",
|
||||
name: "security",
|
||||
|
@ -525,11 +520,6 @@ export const routes = [
|
|||
name: "relays",
|
||||
component: page(() => import("./pages/admin/relays.vue")),
|
||||
},
|
||||
{
|
||||
path: "/integrations",
|
||||
name: "integrations",
|
||||
component: page(() => import("./pages/admin/integrations.vue")),
|
||||
},
|
||||
{
|
||||
path: "/instance-block",
|
||||
name: "instance-block",
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import type * as firefish from "firefish-js";
|
||||
|
||||
export interface Muted {
|
||||
muted: boolean;
|
||||
matched: string[];
|
||||
|
@ -7,7 +9,7 @@ export interface Muted {
|
|||
const NotMuted = { muted: false, matched: [] };
|
||||
|
||||
function checkLangMute(
|
||||
note: NoteLike,
|
||||
note: firefish.entities.Note,
|
||||
mutedLangs: Array<string | string[]>,
|
||||
): Muted {
|
||||
const mutedLangList = new Set(
|
||||
|
@ -20,7 +22,7 @@ function checkLangMute(
|
|||
}
|
||||
|
||||
function checkWordMute(
|
||||
note: NoteLike,
|
||||
note: firefish.entities.Note,
|
||||
mutedWords: Array<string | string[]>,
|
||||
): Muted {
|
||||
let text = `${note.cw ?? ""} ${note.text ?? ""}`;
|
||||
|
@ -72,15 +74,12 @@ function checkWordMute(
|
|||
}
|
||||
|
||||
export function getWordSoftMute(
|
||||
note: Record<string, any>,
|
||||
me: Record<string, any> | null | undefined,
|
||||
note: firefish.entities.Note,
|
||||
meId: string | null | undefined,
|
||||
mutedWords: Array<string | string[]>,
|
||||
mutedLangs: Array<string | string[]>,
|
||||
): Muted {
|
||||
// 自分自身
|
||||
if (me && note.userId === me.id) {
|
||||
return NotMuted;
|
||||
}
|
||||
if (note.userId === meId) return NotMuted;
|
||||
|
||||
if (mutedWords.length > 0) {
|
||||
const noteMuted = checkWordMute(note, mutedWords);
|
||||
|
|
|
@ -41,23 +41,23 @@ export const defaultStore = markRaw(
|
|||
default: 0,
|
||||
},
|
||||
tlHomeHintClosed: {
|
||||
where: "device",
|
||||
where: "account",
|
||||
default: false,
|
||||
},
|
||||
tlLocalHintClosed: {
|
||||
where: "device",
|
||||
where: "account",
|
||||
default: false,
|
||||
},
|
||||
tlRecommendedHintClosed: {
|
||||
where: "device",
|
||||
where: "account",
|
||||
default: false,
|
||||
},
|
||||
tlSocialHintClosed: {
|
||||
where: "device",
|
||||
where: "account",
|
||||
default: false,
|
||||
},
|
||||
tlGlobalHintClosed: {
|
||||
where: "device",
|
||||
where: "account",
|
||||
default: false,
|
||||
},
|
||||
keepCw: {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"api": "pnpm api-extractor run --local --verbose",
|
||||
"api-prod": "pnpm api-extractor run --verbose",
|
||||
"api-doc": "pnpm api-documenter markdown -i ./etc/",
|
||||
"lint": "pnpm biome check --apply **/*.ts",
|
||||
"lint": "pnpm biome check --apply *.ts",
|
||||
"format": "pnpm biome format --write **/*.ts",
|
||||
"jest": "jest --coverage --detectOpenHandles",
|
||||
"test": "pnpm jest && pnpm tsd"
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"build": "pnpm vite build --emptyOutDir",
|
||||
"build:debug": "pnpm run build",
|
||||
"watch": "pnpm swc src -d built -D -w",
|
||||
"lint": "pnpm biome check **/*.ts --apply",
|
||||
"lint": "pnpm biome check *.ts --apply",
|
||||
"format": "pnpm biome format * --write"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -283,7 +283,7 @@ importers:
|
|||
specifier: 7.0.2
|
||||
version: 7.0.2(@types/koa@2.13.8)(ejs@3.1.9)(pug@3.0.2)
|
||||
megalodon:
|
||||
specifier: ^8.1.1
|
||||
specifier: 8.1.1
|
||||
version: 8.1.1
|
||||
meilisearch:
|
||||
specifier: 0.34.1
|
||||
|
@ -315,9 +315,6 @@ importers:
|
|||
nsfwjs:
|
||||
specifier: 2.4.2
|
||||
version: 2.4.2(@tensorflow/tfjs@4.2.0)
|
||||
oauth:
|
||||
specifier: ^0.10.0
|
||||
version: 0.10.0
|
||||
opencc-js:
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5
|
||||
|
|
Loading…
Reference in a new issue