Merge branch 'develop' into feat/scylladb

This commit is contained in:
ThatOneCalculator 2023-10-16 18:33:16 -07:00
commit 57df63c308
No known key found for this signature in database
GPG key ID: 8703CACD01000000
82 changed files with 379 additions and 1798 deletions

View file

@ -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> -->

View file

@ -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

View file

@ -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`.

View file

@ -312,9 +312,6 @@ dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "الصفحات"
integration: "التكامل"
connectService: "اتصل"
disconnectService: "اقطع الاتصال"
enableLocalTimeline: "تفعيل الخيط المحلي"
enableGlobalTimeline: "تفعيل الخيط الزمني الشامل"
disablingTimelinesInfo: "سيتمكن المديرون والمشرفون من الوصول إلى كل الخيوط الزمنية حتى وإن لم تفعّل."

View file

@ -316,9 +316,6 @@ dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "পৃষ্ঠা"
integration: "ইন্টিগ্রেশন"
connectService: "সংযুক্ত করুন"
disconnectService: "সংযোগ বিচ্ছিন্ন করুন"
enableLocalTimeline: "স্থানীয় টাইমলাইন চালু করুন"
enableGlobalTimeline: "গ্লোবাল টাইমলাইন চালু করুন"
disablingTimelinesInfo: "আপনি এই টাইমলাইনগুলি বন্ধ করলেও প্রশাসক এবং মডারেটররা এই টাইমলাইনগুলি ব্যাবহার করতে পারবে"

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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: Μέγεθος Αποθηκευτικού Χώρου ανά απομακρυσμένο μέλος

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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: "アバター画像を変更"

View file

@ -317,9 +317,6 @@ dayX: "{day}日"
monthX: "{month}月"
yearX: "{year}年"
pages: "ページ"
integration: "連携"
connectService: "つなげるで"
disconnectService: "切るで"
enableLocalTimeline: "ローカルタイムラインを使えるようにする"
enableGlobalTimeline: "グローバルタイムラインを使えるようにする"
disablingTimelinesInfo: "ここらへんのタイムラインを使えんようにしてしもても、管理者とモデレーターは使えるままになってるで、そうやなかったら不便やからな。"

View file

@ -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"

View file

@ -323,9 +323,6 @@ dayX: "{day}일"
monthX: "{month}월"
yearX: "{year}년"
pages: "페이지"
integration: "연동"
connectService: "계정 연동"
disconnectService: "계정 연동 해제"
enableLocalTimeline: "로컬 타임라인 활성화"
enableGlobalTimeline: "글로벌 타임라인 활성화"
disablingTimelinesInfo: "특정 타임라인을 비활성화하더라도 관리자 및 모더레이터는 계속 사용할 수 있습니다."

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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."

View file

@ -335,9 +335,6 @@ dayX: "{day} день"
monthX: "{month} месяц"
yearX: "{year} год"
pages: "Страницы"
integration: "Интеграции"
connectService: "Подключиться"
disconnectService: "Отключиться"
enableLocalTimeline: "Включить локальную ленту"
enableGlobalTimeline: "Включить глобальную ленту"
disablingTimelinesInfo: "У администраторов и модераторов есть доступ ко всем лентам,

View file

@ -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é."

View file

@ -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

View file

@ -327,9 +327,6 @@ dayX: "{วัน}"
monthX: "{เดือน}"
yearX: "{ปี}"
pages: "หน้า"
integration: "รวบรวม"
connectService: "เชื่อมต่อ"
disconnectService: "ตัดการเชื่อมต่อ"
enableLocalTimeline: "เปิดใช้งานไทม์ไลน์ในพื้นที่"
enableGlobalTimeline: "เปิดใช้งานไทม์ไลน์ทั่วโลก"
disablingTimelinesInfo: "ผู้ดูแลระบบและผู้ควบคุมจะสามารถเข้าถึงไทม์ไลน์ทั้งหมด ถึงแม้ว่าจะไม่ได้เปิดใช้งานก็ตาม"

View file

@ -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

View file

@ -336,9 +336,6 @@ dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "Сторінки"
integration: "Інтеграції"
connectService: "Під’єднати"
disconnectService: "Відключитися"
enableLocalTimeline: "Увімкнути локальну стрічку"
enableGlobalTimeline: "Увімкнути глобальну стрічку"
disablingTimelinesInfo: "Адміністратори та модератори завжди мають доступ до всіх

View file

@ -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

View file

@ -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: 点击显示模块模式

View file

@ -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"
的格式建構端點EndpointURL。
indexable: 登錄至貼文搜尋引擎
origin: 來源
objectStorageS3ForcePathStyle: 使用基於路徑的端點EndpointURL
clickToShowPatterns: 點擊顯示模組模式Module Pattern

View file

@ -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),
]
}
}

View file

@ -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,
}

View file

@ -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")]

View file

@ -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")]

View file

@ -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",

View file

@ -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

View file

@ -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;

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -459,11 +459,6 @@ export const packedMeDetailedOnlySchema = {
nullable: false,
optional: false,
},
integrations: {
type: "object",
nullable: true,
optional: false,
},
mutedWords: {
type: "array",
nullable: false,

View file

@ -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がありそうならそっちも見る

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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) {

View file

@ -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,

View file

@ -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,

View file

@ -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;
}

View file

@ -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",
},

View file

@ -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",
},

View file

@ -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,

View file

@ -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,

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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

View file

@ -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,
),

View file

@ -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,
),

View file

@ -268,7 +268,7 @@ const isDeleted = ref(false);
const muted = ref(
getWordSoftMute(
note.value,
$i,
$i.id,
defaultStore.state.mutedWords,
defaultStore.state.mutedLangs,
),

View file

@ -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 {

View file

@ -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>

View file

@ -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;
}>();

View file

@ -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,

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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,

View file

@ -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>

View file

@ -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",

View file

@ -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);

View file

@ -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: {

View file

@ -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"

View file

@ -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": {

View file

@ -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