diff --git a/locales/de-DE.yml b/locales/de-DE.yml index ba6704cc..9521870f 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -526,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://.s3.amazonaws.com'. Für GCS o.ä. verwende 'https://storage.googleapis.com/'." -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 „“ oder „:“ angeben." objectStorageRegion: "Region" @@ -1034,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." @@ -1240,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 @@ -2212,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 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 599ea7b0..46a2213b 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -616,7 +616,7 @@ emptyToDisableSmtpAuth: "ユーザー名とパスワードを空欄にするこ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する" smtpSecureInfo: "STARTTLS使用時はオフにします。" testEmail: "配信テスト" -wordMute: "ワードミュート" +wordMute: "単語または言語のミュート" regexpError: "正規表現エラー" regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:" instanceMute: "サーバーミュート" @@ -694,7 +694,7 @@ no: "いいえ" driveFilesCount: "ドライブのファイル数" driveUsage: "ドライブ使用量" noCrawle: "クローラーによるインデックスを拒否" -noCrawleDescription: "検索エンジンにあなたのプロフィールや投稿、ページなどのコンテンツを登録(インデックス)しないよう要請します。" +noCrawleDescription: "Web検索にあなたのプロフィールや投稿、ページなどのコンテンツを登録(インデックス)しないよう要請します。" lockedAccountInfo: "フォローを承認制にしても、投稿の公開範囲を「フォロワー」にしない限り、誰でもあなたの投稿を見られます。" alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にする" loadRawImages: "添付画像のサムネイルをオリジナル画質にする" @@ -811,7 +811,7 @@ instanceSecurity: "サーバーのセキュリティー" secureModeInfo: "認証情報の無いリモートサーバーからのリクエストに応えません。" privateMode: "非公開モード" privateModeInfo: "有効にすると、許可したサーバーのみからリクエストを受け付けます。" -allowedInstances: "許可されたサーバー" +allowedInstances: "許可するサーバー" allowedInstancesDescription: "許可したいサーバーのホストを改行で区切って設定します。非公開モードだけで有効です。" previewNoteText: "本文をプレビュー" customCss: "カスタムCSS" @@ -1518,7 +1518,7 @@ _profile: youCanIncludeHashtags: "ハッシュタグを含められます。" metadata: "追加情報" metadataEdit: "追加情報を編集" - metadataDescription: "プロフィールに追加情報を表示できます。{a}タグまたは{l}タグを{rel}とともに追加すると、プロフィールのリンクを本人認証できます。" + metadataDescription: "プロフィールに追加情報を表示できます。{rel}属性をつけた{a}タグや{l}タグを含むページをリンクすることで、リンクの本人確認もできます!" metadataLabel: "ラベル" metadataContent: "内容" changeAvatar: "アバター画像を変更" @@ -2019,6 +2019,9 @@ postSearch: "このサーバーの投稿検索" indexableDescription: MastodonやFirefishなどの検索機能に、あなたの投稿が表示されるのを許可します。 makePrivate: "秘密にする" makePrivateConfirm: "リモートサーバーに削除リクエストを送信し、投稿の公開範囲を「秘密」にして他の人から見られないようにします。実行しますか?" +clickToShowPatterns: クリックしてトラックを表示 +vibrate: 振動を有効にする +indexable: 投稿検索に登録 _iconSets: bold: "太め" light: "細め" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 5f9db3b4..83c8ac8a 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -937,7 +937,8 @@ _accountDelete: inProgress: "正在删除" _ad: back: "返回" - reduceFrequencyOfThisAd: "减少此广告的频率" + reduceFrequencyOfThisAd: "减少此横幅的频率" + adsBy: 社区横幅(作者:{by}) _forgotPassword: enterEmail: "请输入您注册账号时用的电子邮箱地址,密码重置链接将发送至该邮箱上。" ifNoEmail: "如果您在注册时没有输入电子邮件地址,请联系服务器管理员。" @@ -1996,3 +1997,4 @@ indexable: 可索引的 languageForTranslation: 帖子翻译语言 vibrate: 播放振动 openServerInfo: 点击帖子上的服务器滚动条时显示服务器信息 +clickToShowPatterns: 点击显示模块模式 diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index ed606c35..911cbee3 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -21,7 +21,7 @@ basicSettings: "基本設定" otherSettings: "其他設定" openInWindow: "在新視窗開啟" profile: "個人檔案" -timeline: "時間線" +timeline: "時間軸" noAccountDescription: "此用戶還沒有自我介紹。" login: "登入" loggingIn: "登入中" @@ -149,12 +149,12 @@ flagAsBot: "標記此帳號是機器人" flagAsBotDescription: "如果本帳戶是由程式控制,請啟用此選項。啟用後,會作為標示幫助其他開發者防止機器人之間產生無限互動的行為,並會調整Firefish內部系統將本帳戶識別為機器人。" flagAsCat: "你是喵咪嗎?w😺" flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示!" -flagShowTimelineReplies: "在時間線上顯示貼文的回覆" -flagShowTimelineRepliesDescription: "啟用時,時間線除了顯示用戶的貼文以外,還會顯示用戶對其他貼文的回覆。" +flagShowTimelineReplies: "在時間軸上顯示貼文的回覆" +flagShowTimelineRepliesDescription: "啟用時,時間軸除了顯示用戶的貼文以外,還會顯示用戶對其他貼文的回覆。" autoAcceptFollowed: "自動准予追隨中使用者的追隨請求" addAccount: "添加帳戶" loginFailed: "登入失敗" -showOnRemote: "轉到所在伺服器顯示" +showOnRemote: "開啟來源頁面" general: "一般" wallpaper: "桌布" setWallpaper: "設定桌布" @@ -324,9 +324,9 @@ dayX: "{day}日" monthX: "{month}月" yearX: "{year}年" pages: "頁面" -enableLocalTimeline: "開啟本地時間線" -enableGlobalTimeline: "啟用公開時間線" -disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和板主仍可訪問所有的時間線。" +enableLocalTimeline: "開啟本地時間軸" +enableGlobalTimeline: "啟用公開時間軸" +disablingTimelinesInfo: "即使您關閉了時間軸功能,管理員和板主仍可訪問所有的時間軸。" registration: "註冊" enableRegistration: "開啟新使用者註冊" invite: "邀請" @@ -386,7 +386,7 @@ administrator: "管理員" token: "權杖" twoStepAuthentication: "兩階段驗證" moderator: "板主" -moderation: "言論調節" +moderation: "管理" nUsersMentioned: "提到了{n}" securityKey: "安全金鑰" securityKeyName: "金鑰名稱" @@ -483,7 +483,7 @@ promotion: "推廣" promote: "推廣" numberOfDays: "有效天數" hideThisNote: "隱藏此貼文" -showFeaturedNotesInTimeline: "在時間線上顯示熱門推薦" +showFeaturedNotesInTimeline: "在時間軸上顯示熱門推薦" objectStorage: "Object Storage (物件儲存)" useObjectStorage: "使用Object Storage" objectStorageBaseUrl: "根URL" @@ -503,7 +503,7 @@ objectStorageUseProxyDesc: "如果不使用代理進行API連接,請關閉" objectStorageSetPublicRead: "上傳時設定為\"public-read\"" serverLogs: "伺服器日誌" deleteAll: "刪除所有記錄" -showFixedPostForm: "於時間線頁頂顯示「發送貼文」方框" +showFixedPostForm: "於時間軸頁頂顯示「發送貼文」方框" newNoteRecived: "發現新的貼文" sounds: "音效" listen: "聆聽" @@ -671,7 +671,7 @@ no: "取消" driveFilesCount: "雲端硬碟檔案數量" driveUsage: "雲端硬碟使用量" noCrawle: "拒絕搜尋引擎索引" -noCrawleDescription: "要求網路搜尋引擎不要索引你的個人資料頁、貼文及頁面等。" +noCrawleDescription: "要求外部搜尋引擎不要收錄(索引)你的內容(個人檔案、貼文、頁面等)。" lockedAccountInfo: "即使你通過了追隨者請求,除非你將貼文的可見性設定為 「追隨者」,否則任何人都能看見你的貼文。" alwaysMarkSensitive: "默認將圖像/影像標記為敏感內容" loadRawImages: "以原始圖檔顯示附件圖檔的縮圖" @@ -689,7 +689,7 @@ experimentalFeatures: "實驗中的功能" developer: "開發者" makeExplorable: "使自己的帳戶能夠在“探索”頁面中顯示" makeExplorableDescription: "如果關閉,帳戶將不會被顯示在\"探索\"頁面中。" -showGapBetweenNotesInTimeline: "分開顯示時間線上的貼文" +showGapBetweenNotesInTimeline: "分開顯示時間軸上的貼文" duplicate: "複製" left: "左" center: "置中" @@ -735,7 +735,7 @@ inChannelSearch: "頻道内搜尋" useReactionPickerForContextMenu: "點擊右鍵開啟反應工具欄" typingUsers: "{users}輸入中" jumpToSpecifiedDate: "跳轉到特定日期" -showingPastTimeline: "顯示過往的時間線" +showingPastTimeline: "顯示過往的時間軸" clear: "清除" markAllAsRead: "全部標示為已讀" goBack: "返回" @@ -823,7 +823,7 @@ unmuteThread: "將貼文串的靜音解除" ffVisibility: "連接的公開範圍" ffVisibilityDescription: "您可以設定您的關注/關注者資訊的公開範圍。" continueThread: "查看更多貼文" -deleteAccountConfirm: "將要刪除帳戶。是否確定?" +deleteAccountConfirm: "此帳戶將被刪除,是否繼續?" incorrectPassword: "密碼錯誤。" voteConfirm: "確定投給「{choice}」?" hide: "隱藏" @@ -934,6 +934,7 @@ _accountDelete: _ad: back: "返回" reduceFrequencyOfThisAd: "降低此橫幅的頻率" + adsBy: 社群橫幅(作者:{by}) _forgotPassword: enterEmail: "請輸入您的帳戶註冊的電子郵件地址。 密碼重置連結將被發送到該電子郵件地址。" ifNoEmail: "如果您還沒有註冊您的電子郵件地址,請聯繫管理員。" @@ -1111,14 +1112,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: @@ -1230,16 +1231,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的! 你可能會覺得有點複雜,但你很快就會明白的。" @@ -1328,7 +1329,7 @@ _weekday: _widgets: memo: "備忘錄" notifications: "通知" - timeline: "時間線" + timeline: "時間軸" calendar: "行事曆" trends: "發燒貼文" clock: "時鐘" @@ -1384,9 +1385,9 @@ _poll: remainingSeconds: "{s}秒後截止" _visibility: public: "公開" - publicDescription: "發佈至公開時間線" + publicDescription: "發佈至公開時間軸" home: "不在主頁顯示" - homeDescription: "僅發送至首頁的時間線" + homeDescription: "僅發送至首頁的時間軸" followers: "追隨者" followersDescription: "僅發佈至關注者" specified: "指定使用者" @@ -1787,6 +1788,7 @@ _notification: renote: "轉發" reacted: 對您的貼文做出了反應 renoted: 轉發了您的貼文 + voted: 投了票 _deck: alwaysShowMainColumn: "總是顯示主欄" columnAlign: "對齊欄位" @@ -1810,7 +1812,7 @@ _deck: main: "主列" widgets: "小工具" notifications: "通知" - tl: "時間線" + tl: "時間軸" antenna: "天線" list: "清單" mentions: "提及" @@ -1842,7 +1844,7 @@ accountMoved: '該使用者已遷移至新帳戶:' showAds: 顯示社群橫幅 noThankYou: 不用了,謝謝 selectInstance: 選擇伺服器 -enableRecommendedTimeline: 啟用推薦時間線 +enableRecommendedTimeline: 啟用推薦時間軸 antennaInstancesDescription: 分行列出一個伺服器 moveTo: 遷移此帳戶到新帳戶 moveToLabel: '請輸入你將會遷移到的帳戶:' @@ -1874,6 +1876,7 @@ silenced: 已靜音 _experiments: title: 試驗功能 enablePostImports: 啟用匯入貼文的功能 + postImportsCaption: 允許用戶從舊有的Firefish・Misskey・Mastodon・Akkoma・Pleroma帳號匯入貼文。在伺服器佇列堵塞時匯入貼文可能會導致載入速度變慢。 findOtherInstance: 找找另一個伺服器 noGraze: 瀏覽器擴充元件 "Graze for Mastodon" 會與Firefish發生衝突,請停用該擴充元件。 userSaysSomethingReasonRenote: '{name} 轉發了包含 {reason} 的貼文' @@ -1895,7 +1898,7 @@ pushNotification: 推送通知 subscribePushNotification: 啟用推送通知 unsubscribePushNotification: 停用推送通知 pushNotificationAlreadySubscribed: 推送通知已經啟用 -recommendedInstancesDescription: 以每行分隔的推薦伺服器出現在推薦的時間線中。 +recommendedInstancesDescription: 推薦的伺服器(將顯示在推薦時間軸中),一行一個。 searchPlaceholder: 在 Firefish 上搜尋 cw: 內容警告 selectChannel: 選擇一個頻道 @@ -1903,9 +1906,9 @@ newer: 較新 older: 較舊 jumpToPrevious: 跳到上一個 removeReaction: 移除你的反應 -listsDesc: 清單可以創建一個只有您指定用戶的時間線。 可以從時間線頁面訪問它們。 +listsDesc: 清單可以創建一個只有您指定用戶的時間軸。 可以從時間軸頁面訪問它們。 flagSpeakAsCatDescription: 在喵咪模式下你的貼文會被喵化ヾ(•ω•`)o -antennasDesc: "天線會顯示符合您設置條件的新貼文!\n 可以從時間線訪問它們。" +antennasDesc: "天線會顯示符合您設置條件的新貼文!\n 可以從時間軸訪問它們。" expandOnNoteClick: 點擊以打開貼文 expandOnNoteClickDesc: 即使停用,您仍然可以從右鍵選單或單擊發文時間來打開貼文。 hiddenTagsDescription: '列出您希望隱藏趨勢和探索的主題標籤(不帶 #)。 隱藏的主題標籤仍然可以通過其他方式發現。' @@ -1924,7 +1927,7 @@ seperateRenoteQuote: 分開轉發及引用的按鈕 clipsDesc: 摘錄就像一個可以分享的書籤。 你可以從每個貼文的菜單創建新摘錄或將貼文加入已有的摘錄。 noteId: 貼文 ID sendModMail: 發送審核通知 -enableIdenticonGeneration: 啟用碎片生成 +enableIdenticonGeneration: 啟用Identicon生成 enableServerMachineStats: 啟用伺服器硬體統計資訊 reactionPickerSkinTone: 首選表情符號膚色 indexFromDescription: 留空以索引每個貼文 @@ -1965,10 +1968,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) @@ -2012,3 +2022,9 @@ _iconSets: regular: 標準 fill: 填滿 duotone: 雙色 +objectStorageS3ForcePathStyleDesc: 以 "s3.amazonaws.com//" 而非 ".s3.amazonaws.com" + 的格式建構端點(Endpoint)URL。 +indexable: 登錄至貼文搜尋引擎 +origin: 來源 +objectStorageS3ForcePathStyle: 使用基於路徑的端點(Endpoint)URL +clickToShowPatterns: 點擊顯示模組模式(Module Pattern) diff --git a/neko/UPSTREAM_COMMIT_ID b/neko/UPSTREAM_COMMIT_ID index 9e1dc9f5..429cb448 100644 --- a/neko/UPSTREAM_COMMIT_ID +++ b/neko/UPSTREAM_COMMIT_ID @@ -1 +1 @@ -696d3c6255b3608a8de59fcac5aea3d08b2eeebe +0a44d0652244240aa50fbae3ea88c616c8e1c136 diff --git a/package.json b/package.json index 6e477868..a39c3735 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "firefish", - "version": "1.0.5-dev18", + "version": "1.0.5-dev19", "codename": "aqua", "repository": { "type": "git", "url": "https://code.naskya.net/naskya/firefish" }, - "packageManager": "pnpm@8.8.0", + "packageManager": "pnpm@8.9.2", "private": true, "scripts": { "rebuild": "pnpm run clean && pnpm run build", @@ -57,7 +57,7 @@ "gulp-replace": "1.1.4", "gulp-terser": "2.1.0", "install-peers": "^1.0.4", - "pnpm": "8.8.0", + "pnpm": "8.9.2", "typescript": "5.2.2" } } diff --git a/packages/backend/src/misc/check-hit-antenna.ts b/packages/backend/src/misc/check-hit-antenna.ts index bff919ee..941417cf 100644 --- a/packages/backend/src/misc/check-hit-antenna.ts +++ b/packages/backend/src/misc/check-hit-antenna.ts @@ -9,6 +9,7 @@ import { Cache } from "./cache.js"; import { getWordHardMute } from "./check-word-mute.js"; const blockingCache = new Cache("blocking", 60 * 5); +const mutedWordsCache = new Cache("mutedWords", 60 * 5); export async function checkHitAntenna( antenna: Antenna, @@ -17,33 +18,16 @@ export async function checkHitAntenna( antennaUserFollowing: User["id"][], ): Promise { if (note.visibility === "specified") 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 ( - await getWordHardMute( - note, - antenna.userId, - ( - await UserProfiles.findOneBy({ userId: antenna.userId }) - )?.mutedWords, - ) - ) - return false; + if (antenna.withFile) { + if (note.fileIds && note.fileIds.length === 0) return false; + } + if (!antenna.withReplies && note.replyId != null) return false; if (note.visibility === "followers" || note.visibility === "home") { if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false; } - if (!antenna.withReplies && note.replyId != null) return false; - if (antenna.src === "users") { const accts = antenna.users.map((x) => { const { username, host } = Acct.parse(x); @@ -107,9 +91,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 diff --git a/packages/backend/src/misc/check-word-mute.ts b/packages/backend/src/misc/check-word-mute.ts index fe02fb0f..5686aef2 100644 --- a/packages/backend/src/misc/check-word-mute.ts +++ b/packages/backend/src/misc/check-word-mute.ts @@ -59,10 +59,7 @@ export async function getWordHardMute( meId: string | null | undefined, mutedWords?: Array, ): Promise { - // 自分自身 - if (note.userId === meId || mutedWords == null) { - return false; - } + if (note.userId === meId || mutedWords == null) return false; if (mutedWords.length > 0) { return ( diff --git a/packages/backend/src/queue/processors/inbox.ts b/packages/backend/src/queue/processors/inbox.ts index a5a94b5b..15f75af6 100644 --- a/packages/backend/src/queue/processors/inbox.ts +++ b/packages/backend/src/queue/processors/inbox.ts @@ -90,11 +90,25 @@ export default async (job: Bull.Job): Promise => { } // 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がありそうならそっちも見る diff --git a/packages/backend/src/remote/activitypub/check-fetch.ts b/packages/backend/src/remote/activitypub/check-fetch.ts index c885b4a1..b7fa179a 100644 --- a/packages/backend/src/remote/activitypub/check-fetch.ts +++ b/packages/backend/src/remote/activitypub/check-fetch.ts @@ -86,11 +86,25 @@ export async function checkFetch(req: IncomingMessage): Promise { } // 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; } diff --git a/packages/backend/src/remote/activitypub/db-resolver.ts b/packages/backend/src/remote/activitypub/db-resolver.ts index a710b9f1..39f3c6e4 100644 --- a/packages/backend/src/remote/activitypub/db-resolver.ts +++ b/packages/backend/src/remote/activitypub/db-resolver.ts @@ -17,7 +17,7 @@ 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"; const publicKeyCache = new Cache("publicKey", 60 * 30); const publicKeyByUserIdCache = new Cache( @@ -151,7 +151,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, @@ -203,4 +203,15 @@ export default class DbResolver { key, }; } + + public async refetchPublicKeyForApId( + user: CacheableRemoteUser, + ): Promise { + await updatePerson(user.uri!, undefined, undefined, user); + const key = await UserPublickeys.findOneBy({ userId: user.id }); + if (key != null) { + await publicKeyByUserIdCache.set(user.id, key); + } + return key; + } } diff --git a/packages/backend/src/server/api/endpoints/i/known-as.ts b/packages/backend/src/server/api/endpoints/i/known-as.ts index 0d0c0618..384cbd7c 100644 --- a/packages/backend/src/server/api/endpoints/i/known-as.ts +++ b/packages/backend/src/server/api/endpoints/i/known-as.ts @@ -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", }, diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts index 088c2ff3..fd4ef6b1 100644 --- a/packages/backend/src/server/api/endpoints/i/move.ts +++ b/packages/backend/src/server/api/endpoints/i/move.ts @@ -48,12 +48,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", }, diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 2f117087..e750ac92 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -67,9 +67,8 @@ export default class extends Channel { // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる if ( - this.user && this.userProfile && - (await getWordHardMute(note, this.user.id, this.userProfile.mutedWords)) + (await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords)) ) return; diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 031d7bab..5aafe865 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -66,9 +66,8 @@ export default class extends Channel { // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる if ( - this.user && this.userProfile && - (await getWordHardMute(note, this.user.id, this.userProfile.mutedWords)) + (await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords)) ) return; diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 06aef92a..2ec53a3f 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -83,9 +83,8 @@ export default class extends Channel { // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる if ( - this.user && this.userProfile && - (await getWordHardMute(note, this.user.id, this.userProfile.mutedWords)) + (await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords)) ) return; diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index e64ba215..40e38c24 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -59,9 +59,8 @@ export default class extends Channel { // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる if ( - this.user && this.userProfile && - (await getWordHardMute(note, this.user.id, this.userProfile.mutedWords)) + (await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords)) ) return; diff --git a/packages/backend/src/server/api/stream/channels/recommended-timeline.ts b/packages/backend/src/server/api/stream/channels/recommended-timeline.ts index 358a43dd..bbbc4e7a 100644 --- a/packages/backend/src/server/api/stream/channels/recommended-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/recommended-timeline.ts @@ -81,9 +81,8 @@ export default class extends Channel { // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる if ( - this.user && this.userProfile && - (await getWordHardMute(note, this.user.id, this.userProfile.mutedWords)) + (await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords)) ) return; diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 4a48aa4c..3602192c 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -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, diff --git a/packages/client/src/components/MkFolder.vue b/packages/client/src/components/MkFolder.vue index 4ad2ed6e..c54dc3ab 100644 --- a/packages/client/src/components/MkFolder.vue +++ b/packages/client/src/components/MkFolder.vue @@ -10,15 +10,15 @@ :aria-controls="bodyId" > import { defineComponent } from "vue"; import { getUniqueId } from "@/os"; -// import { defaultStore } from "@/store"; -// import icon from "@/scripts/icon"; +import { defaultStore } from "@/store"; +import icon from "@/scripts/icon"; const localStoragePrefix = "ui:folder:"; @@ -62,6 +62,8 @@ export default defineComponent({ localStoragePrefix + this.persistKey, ) === "t" : this.expanded, + defaultStore, + icon, }; }, watch: { diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue index 45fa4fa1..320ab6be 100644 --- a/packages/client/src/components/MkNote.vue +++ b/packages/client/src/components/MkNote.vue @@ -359,7 +359,7 @@ const isDeleted = ref(false); const muted = ref( getWordSoftMute( note.value, - $i, + $i.id, defaultStore.state.mutedWords, defaultStore.state.mutedLangs, ), diff --git a/packages/client/src/components/MkNoteDetailed.vue b/packages/client/src/components/MkNoteDetailed.vue index 46ffbffa..873199c9 100644 --- a/packages/client/src/components/MkNoteDetailed.vue +++ b/packages/client/src/components/MkNoteDetailed.vue @@ -234,7 +234,7 @@ const isDeleted = ref(false); const muted = ref( getWordSoftMute( note.value, - $i, + $i.id, defaultStore.state.mutedWords, defaultStore.state.mutedLangs, ), diff --git a/packages/client/src/components/MkNoteSub.vue b/packages/client/src/components/MkNoteSub.vue index 97a2618d..1a8a665d 100644 --- a/packages/client/src/components/MkNoteSub.vue +++ b/packages/client/src/components/MkNoteSub.vue @@ -268,7 +268,7 @@ const isDeleted = ref(false); const muted = ref( getWordSoftMute( note.value, - $i, + $i.id, defaultStore.state.mutedWords, defaultStore.state.mutedLangs, ), diff --git a/packages/client/src/components/MkNotification.vue b/packages/client/src/components/MkNotification.vue index bd6a620d..5b64fc54 100644 --- a/packages/client/src/components/MkNotification.vue +++ b/packages/client/src/components/MkNotification.vue @@ -507,12 +507,9 @@ useTooltip(reactionRef, (showing) => { overflow: hidden; text-overflow: ellipsis; - // > span:first-child { - // opacity: 0.7; - // &::after { - // content: ": "; - // } - // } + // > span:first-child { + // opacity: 0.7; + // } > i { vertical-align: super; diff --git a/packages/client/src/components/MkReactedUsers.vue b/packages/client/src/components/MkReactedUsers.vue index 0129a04a..f906c1b0 100644 --- a/packages/client/src/components/MkReactedUsers.vue +++ b/packages/client/src/components/MkReactedUsers.vue @@ -42,11 +42,11 @@ const props = defineProps<{ }>(); const note = ref(); -const tab = ref(); +const tab = ref(null); const reactions = ref(); const users = ref(); -watch(tab, async () => { +async function updateUsers(): void { const res = await os.api("notes/reactions", { noteId: props.noteId, type: tab.value, @@ -54,15 +54,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(); }); }); diff --git a/packages/client/src/components/form/folder.vue b/packages/client/src/components/form/folder.vue index 9446dabe..c289701d 100644 --- a/packages/client/src/components/form/folder.vue +++ b/packages/client/src/components/form/folder.vue @@ -18,11 +18,14 @@