diff --git a/COPYING b/COPYING index 27aeb01ac..5b35f69f2 100644 --- a/COPYING +++ b/COPYING @@ -28,6 +28,10 @@ Machine learning model for sensitive images by Infinite Red, Inc. License: MIT https://github.com/infinitered/nsfwjs/blob/master/LICENSE +Chiptune2.js by Simon Gündling +License: MIT +https://github.com/deskjet/chiptune2.js#license + Licenses for all softwares and software libraries installed via the Node Package Manager ("npm") can be found by running the following shell command in the root directory of this repository: pnpm licenses list diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 7bbde3807..4c9648739 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -1179,7 +1179,7 @@ emptyToDisableSmtpAuth: Deixa el nom d'usuari i la contrasenya sense emplenar pe desactivar la verificació SMTP smtpSecureInfo: Desactiva això quant facis servir STARTTLS testEmail: Envia un correu electrònic de verificació -wordMute: Silenciar paraules +wordMute: Paraules i llenguatge silenciats regexpError: Error a la Expressió Regular regexpErrorDescription: 'Hi ha un error a la expressió regular a la línea {line} de la teva {tab} de paraules silenciades:' @@ -2040,6 +2040,13 @@ _wordMute: s'afegeixin a la línia de temps. A més, aquestes publicacions no s'afegiran a la línia de temps encara que es modifiquin les condicions. mutedNotes: Publicacions silenciades + muteLangsDescription2: Fes servir el codi del l'idioma. Per exemple en, fr, ja, + zh. + lang: Idioma + langDescription: Amagar les publicacions que coincideixin amb l'idioma a la línia + de temps. + muteLangs: Llenguatges silenciats + muteLangsDescription: Separar amb espais o línies no es per una condició OR. _auth: shareAccessAsk: Estàs segur que vols autoritzar aquesta aplicació per accedir al teu compte? @@ -2192,3 +2199,4 @@ indexable: Indexable languageForTranslation: Idioma de traducció d'articles openServerInfo: Mostra la informació del servidor fent clic al símbol del servidor en un missatge +vibrate: Activar vibracions diff --git a/locales/en-US.yml b/locales/en-US.yml index 4b174bc2b..a5574f2bc 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -132,6 +132,7 @@ rememberNoteVisibility: "Remember post visibility settings" attachCancel: "Remove attachment" markAsSensitive: "Mark as NSFW" unmarkAsSensitive: "Unmark as NSFW" +clickToShowPatterns: "Click to show module patterns" enterFileName: "Enter filename" mute: "Mute" unmute: "Unmute" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 4aca0eb2c..da599a8fb 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -637,7 +637,7 @@ emptyToDisableSmtpAuth: "Kosongkan nama pengguna dan kata sandi untuk menonaktif smtpSecure: "Gunakan SSL/TLS implisit untuk koneksi SMTP" smtpSecureInfo: "Matikan ini ketika menggunakan STARTTLS" testEmail: "Tes pengiriman surel" -wordMute: "Bisukan kata" +wordMute: "Bisukan kata dan bahasa" regexpError: "Kesalahan ekspresi reguler" regexpErrorDescription: "Galat terjadi pada baris {line} ekspresi reguler dari {tab} kata yang dibisukan:" @@ -1135,6 +1135,12 @@ _wordMute: soft: "Lembut" hard: "Keras" mutedNotes: "Postingan yang dibisukan" + muteLangsDescription2: Gunakan kode bahasa misalnya en, fr, ja, zh. + lang: Bahasa + langDescription: Sembunyikan postingan yang cocok dengan bahasa yang ditetapkan + dari timeline. + muteLangs: Bahasa yang dibisukan + muteLangsDescription: Pisahkan dengan spasi atau jeda baris untuk kondisi ATAU. _instanceMute: instanceMuteDescription: "Pengaturan ini akan membisukan postingan/pembagian apa saja dari server yang terdaftar, termasuk pengguna yang membalas pengguna lain @@ -2175,3 +2181,4 @@ indexable: Dapat diindeks languageForTranslation: Bahasa terjemahan kiriman openServerInfo: Tampilkan informasi server dengan mengeklik ticker server di sebuah kiriman +vibrate: Putar getaran diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 574afa851..9df04022b 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -120,6 +120,7 @@ rememberNoteVisibility: "Lembrar das configurações de visibilidade de notas" attachCancel: "Remover anexo" markAsSensitive: "Marcar como sensível" unmarkAsSensitive: "Desmarcar como sensível" +clickToShowPatterns: "Clique para mostrar os padrões do módulo" enterFileName: "Digite o nome do ficheiro" mute: "Silenciar" unmute: "Dessilenciar" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index bda13aa90..709156fcf 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -285,7 +285,7 @@ pinnedPagesDescription: Bu sunucunun üst kısmına sabitlemek istediğiniz Sayf yollarını satır sonundan ayırarak girin. enableHcaptcha: hCaptcha'yı Aktif Et notifyAntenna: Yeni gönderileri bildir -recentlyUpdatedUsers: En son aktif kullanıcılar +recentlyUpdatedUsers: En son aktif olan kullanıcılar about: Hakkında twoStepAuthentication: İki-adımlı doğrulama securityKeyName: Anahtar ismi diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 00728c946..fe7612399 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -486,15 +486,16 @@ hideThisNote: "隐藏这条帖子" showFeaturedNotesInTimeline: "在时间线上显示热门推荐" objectStorage: "对象存储" useObjectStorage: "使用对象存储" -objectStorageBaseUrl: "Base URL" -objectStorageBaseUrlDesc: "用于引用的 URL。如果您正在使用 CDN 或反向代理,请指定其 URL。\n例如S3:“https://.s3.amazonaws.com”,GCS:“https://storage.googleapis.com/”,其它同理。" +objectStorageBaseUrl: "根 URL" +objectStorageBaseUrlDesc: "用于引用的 URL。如果您正在使用 CDN 或反向代理,请指定其 URL。\n例如S3:\"https://.s3.amazonaws.com\"\ + ,GCS:\"https://storage.googleapis.com/\",其它同理。" objectStorageBucket: "存储桶" objectStorageBucketDesc: "请指定使用的对象存储服务的存储桶名称。" objectStoragePrefix: "前缀" objectStoragePrefixDesc: "文件将存储在此前缀的目录下。" -objectStorageEndpoint: "Endpoint" -objectStorageEndpointDesc: "如果您使用 AWS S3 请留空。否则请根据您使用的服务商的说明来进行设置,指定 Endpoint 形式为 - \"\" 或 \":\"。" +objectStorageEndpoint: "端点 (Endpoint)" +objectStorageEndpointDesc: "如果您使用 AWS S3 请留空。否则请根据您使用的服务商的说明来进行设置,指定端点 (Endpoint) + 形式为 \"\" 或 \":\"。" objectStorageRegion: "可用区" objectStorageRegionDesc: "指定一个可用区,例如 \"xx-east-1\"。 如果您的对象存储服务没有可用区概念,请将其留空或填写 \"\ us-east-1\"。\n对于 Cloudflare R2,可以填为 \"auto\"。" @@ -502,7 +503,7 @@ objectStorageUseSSL: "使用 SSL" objectStorageUseSSLDesc: "如果不使用 HTTPS 进行 API 连接,请关闭" objectStorageUseProxy: "使用代理" objectStorageUseProxyDesc: "如果您不使用代理进行 API 连接,请将其关闭" -objectStorageSetPublicRead: "上传时设置为 public-read" +objectStorageSetPublicRead: "上传时设置为 \"public-read\"" serverLogs: "服务器日志" deleteAll: "全部删除" showFixedPostForm: "在时间线顶部显示发帖框" @@ -599,7 +600,7 @@ emptyToDisableSmtpAuth: "留空用户名和密码以禁用 SMTP 验证" smtpSecure: "在 SMTP 连接中使用隐式 SSL / TLS" smtpSecureInfo: "使用 STARTTLS 时关闭" testEmail: "邮件发送测试" -wordMute: "文字过滤" +wordMute: "文字和语言过滤" regexpError: "正则表达式错误" regexpErrorDescription: "{tab} 文字过滤的第 {line} 行的正则表达式有错误:" instanceMute: "服务器静音" @@ -608,7 +609,7 @@ makeActive: "启用" display: "显示" copy: "复制" metrics: "指标" -overview: "服务器概况" +overview: "概况" logs: "日志" delayed: "滞后" database: "数据库" @@ -745,7 +746,7 @@ unlikeConfirm: "取消赞?" fullView: "全屏" quitFullView: "退出全屏" addDescription: "添加描述" -userPagePinTip: "在帖子的菜单中选择“置顶”,即可显示该条帖子。" +userPagePinTip: "在帖子的菜单中选择「置顶」,即可在此显示该条帖子。" notSpecifiedMentionWarning: "有未指定的提及" info: "关于" userInfo: "用户信息" @@ -805,7 +806,7 @@ accountDeletionInProgress: "正在删除账号" usernameInfo: "在服务器上唯一标识您的账号的名称。您可以使用字母 (a ~ z, A ~ Z)、数字 (0 ~ 9) 和下划线 (_)。用户名以后不能更改。" aiChanMode: "小蓝模式" keepCw: "保留内容警告" -pubSub: "推送 (Pub)/订阅 (Sub) 账号" +pubSub: "推送 (Pub) / 订阅 (Sub) 账号" lastCommunication: "最近通信" resolved: "已解决" unresolved: "未解决" @@ -1114,7 +1115,7 @@ _wordMute: muteLangs: "过滤语言" muteWordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。" muteWordsDescription2: "将关键字用斜线括起来表示正则表达式。" - muteLangsDescription: "OR 条件用空格,换行符分隔" + muteLangsDescription: "OR 条件用空格或换行符分隔。" muteLangsDescription2: "使用语言代码。例: en, fr, ja, zh." softDescription: "隐藏时间线中指定条件的帖子。" langDescription: "从时间线中隐藏与设置语言匹配的帖子。" @@ -1840,8 +1841,8 @@ customMOTD: 自定义 MOTD(启动屏幕消息) sendPushNotificationReadMessageCaption: 会短暂显示 "{emptyPushNotificationMessage}" 的通知,如果启用,可能会增加您的设备的耗电量。 adminCustomCssWarn: 仅当您知道此设置的作用时才应使用它。输入不正确的值可能会导致每个人的客户端停止正常运行。请在用户设置中进行测试来确保您的 CSS 正常工作。 -customMOTDDescription: 自定义 MOTD(启动屏幕)消息,一行一个,每次用户加载/刷新页面时都会随机显示。 -customSplashIconsDescription: 用换行符隔开的自定义启动屏幕图标的 URL,在用户每次加载/重新载入页面时随机显示。请确保图片是在一个静态的 +customMOTDDescription: 自定义 MOTD(启动屏幕)消息,一行一个,每次用户加载 / 重新加载页面时都会随机显示。 +customSplashIconsDescription: 用换行符隔开的自定义启动屏幕图标的 URL,在用户每次加载 / 重新加载页面时随机显示。请确保图片是在一个静态的 URL 上,最好全部调整为 192x192 的大小。 recommendedInstancesDescription: 推荐的服务器一行一个,它们将出现在推荐时间线中。 splash: 启动画面 @@ -1864,7 +1865,7 @@ customSplashIcons: 自定义启动屏幕图标(urls) alt: 替代文字 pushNotificationNotSupported: 您的浏览器或者服务器不支持推送通知 showAds: 显示社区横幅 -enterSendsMessage: 按回车键发送信息(关闭则是 Ctrl + Retun 发送) +enterSendsMessage: 按回车键发送信息(关闭则是 Ctrl + Return 发送) recommendedInstances: 推荐服务器 updateAvailable: 可能有可用更新! swipeOnMobile: 允许在页面之间滑动 @@ -1977,7 +1978,7 @@ confirm: 确认 importZip: 导入 ZIP exportZip: 导出 ZIP emojiPackCreator: 表情包创建工具 -objectStorageS3ForcePathStyleDesc: 打开此选项可构建格式为 's3.amazonaws.com//' 而非 '.s3.amazonaws.com' +objectStorageS3ForcePathStyleDesc: 打开此选项可构建格式为 "s3.amazonaws.com//" 而非 ".s3.amazonaws.com" 的端点 URL。 objectStorageS3ForcePathStyle: 使用基于路径的端点 URL delete2fa: 禁用 2FA @@ -1990,3 +1991,5 @@ detectPostLanguage: 自动检测语言,并显示外文帖子的翻译按钮 indexableDescription: 允许内置搜索显示您的公开帖子 indexable: 可索引的 languageForTranslation: 帖子翻译语言 +vibrate: 播放振动 +openServerInfo: 点击帖子上的服务器滚动条时显示服务器信息 diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 91dc76d97..4aa1cf3d4 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -12,7 +12,7 @@ ok: "OK" gotIt: "知道了!" cancel: "取消" enterUsername: "輸入使用者名稱" -renotedBy: "{user} 轉傳了" +renotedBy: "{user} 轉發了" noNotes: "無貼文" noNotifications: "沒有通知" instance: "伺服器" @@ -325,7 +325,7 @@ connectService: "己連結" disconnectService: "己斷開" enableLocalTimeline: "開啟本地時間線" enableGlobalTimeline: "啟用公開時間線" -disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和版主始終可以訪問所有的時間線。" +disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和板主仍可訪問所有的時間線。" registration: "註冊" enableRegistration: "開啟新使用者註冊" invite: "邀請" @@ -801,7 +801,7 @@ translatedFrom: "從 {x} 翻譯" accountDeletionInProgress: "正在刪除帳戶" usernameInfo: "在伺服器上您的帳戶是唯一的識別名稱。您可以使用字母 (a ~ z, A ~ Z)、數字 (0 ~ 9) 和下底線 (_)。之後帳戶名是不能更改的。" aiChanMode: "小藍模式" -keepCw: "保持CW" +keepCw: "保持內容警告" pubSub: "Pub/Sub 帳戶" lastCommunication: "最近的通信" resolved: "已解決" @@ -1068,7 +1068,7 @@ _mfm: position: 位置 alwaysPlay: 自動播放所有MFM動畫 positionDescription: 按指定數量移動內容。 - advancedDescription: 如果禁用,則僅允許基本標記,除非正在播放 MFM 動畫 + advancedDescription: 如果停用,僅顯示基礎MFM及正在播放的MFM動畫 advanced: 高級MFM fade: 淡出 foreground: 文字顏色 @@ -1115,6 +1115,11 @@ _wordMute: soft: "軟性靜音" hard: "硬性靜音" mutedNotes: "已靜音的貼文" + muteLangsDescription2: '使用語言代碼。例: en, fr, ja, zh.' + lang: 語言 + langDescription: 將指定語言的貼文從時間線中隱藏。 + muteLangs: 被靜音的語言 + muteLangsDescription: OR條件以空格或換行進行分隔。 _instanceMute: instanceMuteDescription: "包括對被靜音伺服器上的用戶的回覆,被設定的伺服器上所有貼文及轉發都會被靜音。" instanceMuteDescription2: "設定時以換行進行分隔" @@ -1356,9 +1361,9 @@ _cw: files: "{count} 個檔案" _poll: noOnlyOneChoice: "至少需要兩個選項" - choiceN: "選擇{n}" + choiceN: "選項{n}" noMore: "沒辦法再添加選項了" - canMultipleVote: "可以多次投票" + canMultipleVote: "允許複選" expiration: "期限" infinite: "無期限" at: "結束時間" @@ -1367,7 +1372,7 @@ _poll: deadlineTime: "小時" duration: "時長" votesCount: "{n}票" - totalVotes: "一共{n}票" + totalVotes: "總計{n}票" vote: "投票" showResult: "顯示結果" voted: "已投票" @@ -1778,6 +1783,7 @@ _notification: reply: "回覆" renote: "轉發" reacted: 對您的貼文做出了反應 + renoted: 轉發了您的貼文 _deck: alwaysShowMainColumn: "總是顯示主欄" columnAlign: "對齊欄位" @@ -1864,9 +1870,10 @@ silencedInstances: 已靜音的伺服器 silenced: 已靜音 _experiments: title: 試驗功能 + enablePostImports: 啟用匯入貼文的功能 findOtherInstance: 找找另一個伺服器 noGraze: 瀏覽器擴展 "Graze for Mastodon" 會與Firefish發生衝突,請停用該擴展。 -userSaysSomethingReasonRenote: '{name} 轉傳了包含 {reason} 的貼文' +userSaysSomethingReasonRenote: '{name} 轉發了包含 {reason} 的貼文' pushNotificationNotSupported: 你的瀏覽器或伺服器不支援推送通知 accessibility: 輔助功能 userSaysSomethingReasonReply: '{name} 回覆了包含 {reason} 的貼文' @@ -1910,7 +1917,7 @@ channelFederationWarn: 頻道功能尚未與聯邦宇宙連動 swipeOnMobile: 允許以滑動在頁面之間切換 sendPushNotificationReadMessage: 閱讀相關通知或消息後刪除推送通知 image: 圖片 -seperateRenoteQuote: 分別獨立的轉傳及引用按鈕 +seperateRenoteQuote: 分開轉發及引用的按鈕 clipsDesc: 摘錄就像一個可以分享的書籤。 你可以從每個貼文的菜單創建新摘錄或將貼文加入已有的摘錄。 noteId: 貼文 ID sendModMail: 發送審核通知 @@ -1920,7 +1927,7 @@ reactionPickerSkinTone: 首選表情符號膚色 indexFromDescription: 留空以索引每個貼文 preventAiLearning: 防止 AI 機器人抓取 preventAiLearningDescription: 請求第三方 AI 語言模型不要研究您上傳的內容,例如貼文和圖像。 -indexFrom: 從貼文 ID 開始的索引 +indexFrom: 建立此貼文ID以後的索引 isLocked: 該帳戶已獲得以下批准 isModerator: 板主 isAdmin: 管理員 @@ -1928,7 +1935,7 @@ isPatron: Firefish 項目贊助者 silencedWarning: 顯示此頁面是因為這些使用者來自您伺服器管理員已靜音的伺服器,因此他們可能是垃圾訊息。 signupsDisabled: 該伺服器上的註冊當前已被禁用,但您隨時可以在另一台伺服器上註冊!或是您有該伺服器的邀請碼,請在下面輸入。 showPopup: 通過彈出式視窗通知用戶 -showWithSparkles: 閃閃發光的顯示 +showWithSparkles: 讓標題閃閃發光 youHaveUnreadAnnouncements: 您有未讀的公告 donationLink: 連結到贊助頁面 neverShow: 不再顯示 @@ -1963,3 +1970,11 @@ emojiPackCreator: 表情包的作者 importZip: 匯入ZIP delete2fa: 停用二階段認證(2FA) confirm: 確認 +deletePasskeysConfirm: 此帳號的所有通行密鑰及安全密鑰將被完全刪除。此動作無法復原,是否繼續? +deletePasskeys: 刪除通行密鑰 +detectPostLanguage: 自動判定貼文的語言,並在外文貼文顯示翻譯按鈕 +indexableDescription: 允許內建搜尋引擎顯示您的公開貼文 +addRe: 在回覆有內容警告的貼文時,在標題前面加上 "re:" +vibrate: 播放振動 +openServerInfo: 點擊貼文中的伺服器名稱以顯示伺服器資訊 +languageForTranslation: 貼文翻譯語言 diff --git a/neko/UPSTREAM_COMMIT_ID b/neko/UPSTREAM_COMMIT_ID index 3f8d97d1a..73693162d 100644 --- a/neko/UPSTREAM_COMMIT_ID +++ b/neko/UPSTREAM_COMMIT_ID @@ -1 +1 @@ -c17fb8217b88ef3e4d4f0756a458e2114ba47088 +2cd036b102edbf63fadb68e3841e4c912032f993 diff --git a/package.json b/package.json index 836c7cd6d..cd071b09f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firefish", - "version": "1.0.4-beta31", + "version": "1.0.5-dev17", "codename": "aqua", "repository": { "type": "git", diff --git a/packages/backend/migration/1695334243217-add-post-lang.js b/packages/backend/migration/1695334243217-add-post-lang.js new file mode 100644 index 000000000..7e8618953 --- /dev/null +++ b/packages/backend/migration/1695334243217-add-post-lang.js @@ -0,0 +1,13 @@ +export class AddPostLang1695334243217 { + name = "AddPostLang1695334243217"; + + async up(queryRunner) { + await queryRunner.query( + `ALTER TABLE "note" ADD "lang" character varying(10)`, + ); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "lang"`); + } +} diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts index 2a955ee52..6dddf1fff 100644 --- a/packages/backend/src/const.ts +++ b/packages/backend/src/const.ts @@ -68,6 +68,15 @@ export const FILE_TYPE_BROWSERSAFE = [ "audio/x-flac", "audio/flac", "audio/vnd.wave", + + "audio/mod", + "audio/x-mod", + "audio/s3m", + "audio/x-s3m", + "audio/xm", + "audio/x-xm", + "audio/it", + "audio/x-it", ]; /* https://github.com/sindresorhus/file-type/blob/main/supported.js diff --git a/packages/backend/src/mfm/from-html.ts b/packages/backend/src/mfm/from-html.ts index 7c956e905..fbcf31025 100644 --- a/packages/backend/src/mfm/from-html.ts +++ b/packages/backend/src/mfm/from-html.ts @@ -110,9 +110,8 @@ export function fromHtml(html: string, hashtagNames?: string[]): string { } case "h1": { - text += "【"; appendChildren(node.childNodes); - text += "】\n"; + text += "\n"; break; } diff --git a/packages/backend/src/misc/get-reaction-emoji.ts b/packages/backend/src/misc/get-reaction-emoji.ts deleted file mode 100644 index 71521c4ae..000000000 --- a/packages/backend/src/misc/get-reaction-emoji.ts +++ /dev/null @@ -1,28 +0,0 @@ -export default function (reaction: string): string { - switch (reaction) { - case "like": - return "👍"; - case "love": - return "❤️"; - case "laugh": - return "😆"; - case "hmm": - return "🤔"; - case "surprise": - return "😮"; - case "congrats": - return "🎉"; - case "angry": - return "💢"; - case "confused": - return "😥"; - case "rip": - return "😇"; - case "pudding": - return "🍮"; - case "star": - return "⭐"; - default: - return reaction; - } -} diff --git a/packages/backend/src/misc/reaction-lib.ts b/packages/backend/src/misc/reaction-lib.ts index e25b2d661..77524057d 100644 --- a/packages/backend/src/misc/reaction-lib.ts +++ b/packages/backend/src/misc/reaction-lib.ts @@ -4,58 +4,22 @@ import { Emojis } from "@/models/index.js"; import { toPunyNullable } from "./convert-host.js"; import { IsNull } from "typeorm"; -const legacies = new Map([ - ["like", "👍"], - ["love", "❤️"], - ["laugh", "😆"], - ["hmm", "🤔"], - ["surprise", "😮"], - ["congrats", "🎉"], - ["angry", "💢"], - ["confused", "😥"], - ["rip", "😇"], - ["pudding", "🍮"], - ["star", "⭐"], -]); - export async function getFallbackReaction() { const meta = await fetchMeta(); return meta.defaultReaction; } -export function convertLegacyReactions(reactions: Record) { - const _reactions = new Map(); - const decodedReactions = new Map(); +export function convertReactions(reactions: Record) { + const result = new Map(); for (const reaction in reactions) { if (reactions[reaction] <= 0) continue; - let decodedReaction; - if (decodedReactions.has(reaction)) { - decodedReaction = decodedReactions.get(reaction); - } else { - decodedReaction = decodeReaction(reaction); - decodedReactions.set(reaction, decodedReaction); - } - - let emoji = legacies.get(decodedReaction.reaction); - if (emoji) { - _reactions.set(emoji, (_reactions.get(emoji) || 0) + reactions[reaction]); - } else { - _reactions.set( - reaction, - (_reactions.get(reaction) || 0) + reactions[reaction], - ); - } + const decoded = decodeReaction(reaction).reaction; + result.set(decoded, (result.get(decoded) || 0) + reactions[reaction]); } - const _reactions2 = new Map(); - for (const [reaction, count] of _reactions) { - const decodedReaction = decodedReactions.get(reaction); - _reactions2.set(decodedReaction.reaction, count); - } - - return Object.fromEntries(_reactions2); + return Object.fromEntries(result); } export async function toDbReaction( @@ -66,9 +30,7 @@ export async function toDbReaction( reacterHost = toPunyNullable(reacterHost); - // Convert string-type reactions to unicode - const emoji = legacies.get(reaction) || (reaction === "♥️" ? "❤️" : null); - if (emoji) return emoji; + if (reaction === "♥️") return "❤️"; // Allow unicode reactions const match = emojiRegex.exec(reaction); @@ -128,9 +90,3 @@ export function decodeReaction(str: string): DecodedReaction { host: undefined, }; } - -export function convertLegacyReaction(reaction: string): string { - const decoded = decodeReaction(reaction).reaction; - if (legacies.has(decoded)) return legacies.get(decoded)!; - return decoded; -} diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts index 21fe64e90..2b9de38cb 100644 --- a/packages/backend/src/models/entities/note.ts +++ b/packages/backend/src/models/entities/note.ts @@ -66,6 +66,12 @@ export class Note { }) public text: string | null; + @Column("varchar", { + length: 10, + nullable: true, + }) + public lang: string | null; + @Column("varchar", { length: 256, nullable: true, diff --git a/packages/backend/src/models/repositories/note-reaction.ts b/packages/backend/src/models/repositories/note-reaction.ts index 6d1dfbd6f..4075c7d43 100644 --- a/packages/backend/src/models/repositories/note-reaction.ts +++ b/packages/backend/src/models/repositories/note-reaction.ts @@ -2,7 +2,7 @@ import { db } from "@/db/postgre.js"; import { NoteReaction } from "@/models/entities/note-reaction.js"; import { Notes, Users } from "../index.js"; import type { Packed } from "@/misc/schema.js"; -import { convertLegacyReaction } from "@/misc/reaction-lib.js"; +import { decodeReaction } from "@/misc/reaction-lib.js"; import type { User } from "@/models/entities/user.js"; export const NoteReactionRepository = db.getRepository(NoteReaction).extend({ @@ -27,7 +27,7 @@ export const NoteReactionRepository = db.getRepository(NoteReaction).extend({ id: reaction.id, createdAt: reaction.createdAt.toISOString(), user: await Users.pack(reaction.user ?? reaction.userId, me), - type: convertLegacyReaction(reaction.reaction), + type: decodeReaction(reaction.reaction).reaction, ...(opts.withNote ? { // may throw error @@ -41,7 +41,7 @@ export const NoteReactionRepository = db.getRepository(NoteReaction).extend({ src: NoteReaction[], me?: { id: User["id"] } | null | undefined, options?: { - withNote: booleam; + withNote: boolean; }, ): Promise[]> { const reactions = await Promise.allSettled( diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts index f683fea69..7e90b7dee 100644 --- a/packages/backend/src/models/repositories/note.ts +++ b/packages/backend/src/models/repositories/note.ts @@ -14,11 +14,7 @@ import { import type { Packed } from "@/misc/schema.js"; import { nyaize } from "@/misc/nyaize.js"; import { awaitAll } from "@/prelude/await-all.js"; -import { - convertLegacyReaction, - convertLegacyReactions, - decodeReaction, -} from "@/misc/reaction-lib.js"; +import { convertReactions, decodeReaction } from "@/misc/reaction-lib.js"; import type { NoteReaction } from "@/models/entities/note-reaction.js"; import { aggregateNoteEmojis, @@ -27,7 +23,7 @@ import { } from "@/misc/populate-emojis.js"; import { db } from "@/db/postgre.js"; import { IdentifiableError } from "@/misc/identifiable-error.js"; -import { detect as detectLanguage_ } from "tinyld"; +import { detect as detectLanguage } from "tinyld"; export async function populatePoll(note: Note, meId: User["id"] | null) { const poll = await Polls.findOneByOrFail({ noteId: note.id }); @@ -77,7 +73,7 @@ async function populateMyReaction( if (_hint_?.myReactions) { const reaction = _hint_.myReactions.get(note.id); if (reaction) { - return convertLegacyReaction(reaction.reaction); + return decodeReaction(reaction.reaction).reaction; } else if (reaction === null) { return undefined; } @@ -90,7 +86,7 @@ async function populateMyReaction( }); if (reaction) { - return convertLegacyReaction(reaction.reaction); + return decodeReaction(reaction.reaction).reaction; } return undefined; @@ -203,8 +199,6 @@ export const NoteRepository = db.getRepository(Note).extend({ host, ); - const lang = - detectLanguage_(`${note.cw ?? ""}\n${note.text ?? ""}`) ?? "unknown"; const reactionEmoji = await populateEmojis(reactionEmojiNames, host); const packed: Packed<"Note"> = await awaitAll({ id: note.id, @@ -221,7 +215,7 @@ export const NoteRepository = db.getRepository(Note).extend({ note.visibility === "specified" ? note.visibleUserIds : undefined, renoteCount: note.renoteCount, repliesCount: note.repliesCount, - reactions: convertLegacyReactions(note.reactions), + reactions: convertReactions(note.reactions), reactionEmojis: reactionEmoji, emojis: noteEmoji, tags: note.tags.length > 0 ? note.tags : undefined, @@ -264,7 +258,7 @@ export const NoteRepository = db.getRepository(Note).extend({ : undefined, } : {}), - lang: lang, + lang: note.lang, }); if (packed.user.isCat && packed.user.speakAsCat && packed.text) { diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts index 12b2c1997..b2354bba6 100644 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ b/packages/backend/src/remote/activitypub/models/note.ts @@ -53,6 +53,7 @@ import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; import { truncate } from "@/misc/truncate.js"; import { type Size, getEmojiSize } from "@/misc/emoji-meta.js"; import { fetchMeta } from "@/misc/fetch-meta.js"; +import { langmap } from "@/misc/langmap.js"; const logger = apLogger; @@ -247,7 +248,7 @@ export async function createNote( // Quote let quote: Note | undefined | null; - if (note._misskey_quote || note.quoteUrl || note.quoteUri) { + if (note.quoteUrl || note.quoteUri) { const tryResolveNote = async ( uri: string, ): Promise< @@ -284,7 +285,7 @@ export async function createNote( }; const uris = unique( - [note._misskey_quote, note.quoteUrl, note.quoteUri].filter( + [note.quoteUrl, note.quoteUri].filter( (x): x is string => typeof x === "string", ), ); @@ -305,13 +306,24 @@ export async function createNote( // Text parsing let text: string | null = null; + let lang: string | null = null; if ( note.source?.mediaType === "text/x.misskeymarkdown" && typeof note.source?.content === "string" ) { text = note.source.content; - } else if (typeof note._misskey_content !== "undefined") { - text = note._misskey_content; + if (note.contentMap != null) { + const key = Object.keys(note.contentMap)[0]; + lang = Object.keys(langmap).includes(key) + ? key.trim().split("-")[0].split("@")[0] + : null; + } + } else if (note.contentMap != null) { + const entry = Object.entries(note.contentMap)[0]; + lang = Object.keys(langmap).includes(entry[0]) + ? entry[0].trim().split("-")[0].split("@")[0] + : null; + text = htmlToMfm(entry[1], note.tag); } else if (typeof note.content === "string") { text = htmlToMfm(note.content, note.tag); } @@ -380,6 +392,7 @@ export async function createNote( name: note.name, cw, text, + lang, localOnly: false, visibility, visibleUsers, @@ -567,13 +580,24 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) { // Text parsing let text: string | null = null; + let lang: string | null = null; if ( post.source?.mediaType === "text/x.misskeymarkdown" && typeof post.source?.content === "string" ) { text = post.source.content; - } else if (typeof post._misskey_content !== "undefined") { - text = post._misskey_content; + if (post.contentMap != null) { + const key = Object.keys(post.contentMap)[0]; + lang = Object.keys(langmap).includes(key) + ? key.trim().split("-")[0].split("@")[0] + : null; + } + } else if (post.contentMap != null) { + const entry = Object.entries(post.contentMap)[0]; + lang = Object.keys(langmap).includes(entry[0]) + ? entry[0].trim().split("-")[0].split("@")[0] + : null; + text = htmlToMfm(entry[1], post.tag); } else if (typeof post.content === "string") { text = htmlToMfm(post.content, post.tag); } @@ -667,6 +691,9 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) { if (text && text !== note.text) { update.text = text; } + if (lang && lang !== note.lang) { + update.lang = lang; + } if (cw !== note.cw) { update.cw = cw ? cw : null; } diff --git a/packages/backend/src/remote/activitypub/renderer/index.ts b/packages/backend/src/remote/activitypub/renderer/index.ts index dc8e1325e..7d2eb9528 100644 --- a/packages/backend/src/remote/activitypub/renderer/index.ts +++ b/packages/backend/src/remote/activitypub/renderer/index.ts @@ -40,11 +40,9 @@ export const renderActivity = (x: any): IActivity | null => { speakAsCat: "firefish:speakAsCat", // Misskey misskey: "https://misskey-hub.net/ns#", - _misskey_content: "misskey:_misskey_content", - _misskey_quote: "misskey:_misskey_quote", + _misskey_talk: "misskey:_misskey_talk", _misskey_reaction: "misskey:_misskey_reaction", _misskey_votes: "misskey:_misskey_votes", - _misskey_talk: "misskey:_misskey_talk", isCat: "misskey:isCat", // Fedibird fedibird: "http://fedibird.com/ns#", diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts index 2ad2fec9f..4c7650e8c 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -1,4 +1,5 @@ import { In, IsNull } from "typeorm"; +import { detect as detectLanguage } from "tinyld"; import config from "@/config/index.js"; import type { Note, IMentionedRemoteUsers } from "@/models/entities/note.js"; import type { DriveFile } from "@/models/entities/drive-file.js"; @@ -114,6 +115,13 @@ export default async function renderNote( }), ); + const lang = note.lang ?? detectLanguage(text); + const contentMap = lang + ? { + [lang]: content, + } + : null; + const emojis = await getEmojis(note.emojis); const apemojis = emojis.map((emoji) => renderEmoji(emoji)); @@ -152,12 +160,11 @@ export default async function renderNote( attributedTo, summary, content, - _misskey_content: text, + contentMap, source: { content: text, mediaType: "text/x.misskeymarkdown", }, - _misskey_quote: quote, quoteUri: quote, quoteUrl: quote, published: note.createdAt.toISOString(), diff --git a/packages/backend/src/remote/activitypub/type.ts b/packages/backend/src/remote/activitypub/type.ts index ecaf6d687..0e63f3ed5 100644 --- a/packages/backend/src/remote/activitypub/type.ts +++ b/packages/backend/src/remote/activitypub/type.ts @@ -14,6 +14,7 @@ export interface IObject { inReplyTo?: any; replies?: ICollection; content?: string; + contentMap?: obj; name?: string; startTime?: Date; endTime?: Date; @@ -134,7 +135,6 @@ export interface IPost extends IObject { content: string; mediaType: string; }; - _misskey_quote?: string; quoteUrl?: string; quoteUri?: string; _misskey_talk: boolean; @@ -146,7 +146,6 @@ export interface IQuestion extends IObject { content: string; mediaType: string; }; - _misskey_quote?: string; quoteUrl?: string; oneOf?: IQuestionChoice[]; anyOf?: IQuestionChoice[]; diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 542f617b3..150356811 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -108,6 +108,7 @@ export const paramDef = { }, }, text: { type: "string", maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true }, + lang: { type: "string", nullable: true, maxLength: 10 }, cw: { type: "string", nullable: true, maxLength: 100 }, localOnly: { type: "boolean", default: false }, noExtractMentions: { type: "boolean", default: false }, @@ -294,6 +295,7 @@ export default define(meta, paramDef, async (ps, user) => { } : undefined, text: ps.text || undefined, + lang: ps.lang, reply, renote, cw: ps.cw, diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index 8daf44b48..9add57670 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -35,6 +35,8 @@ import renderUpdate from "@/remote/activitypub/renderer/update.js"; import { deliverToRelays } from "@/services/relay.js"; // import { deliverQuestionUpdate } from "@/services/note/polls/update.js"; import { fetchMeta } from "@/misc/fetch-meta.js"; +import { detect as detectLanguage } from "tinyld"; +import { langmap } from "@/misc/langmap.js"; export const meta = { tags: ["notes"], @@ -169,6 +171,7 @@ export const paramDef = { }, }, text: { type: "string", maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true }, + lang: { type: "string", nullable: true, maxLength: 10 }, cw: { type: "string", nullable: true, maxLength: 250 }, localOnly: { type: "boolean", default: false }, noExtractMentions: { type: "boolean", default: false }, @@ -375,6 +378,16 @@ export default define(meta, paramDef, async (ps, user) => { ps.text = null; } + if (ps.lang) { + if (!Object.keys(langmap).includes(ps.lang.trim())) + throw new Error("invalid param"); + ps.lang = ps.lang.trim().split("-")[0].split("@")[0]; + } else if (ps.text) { + ps.lang = detectLanguage(ps.text); + } else { + ps.lang = null; + } + let tags = []; let emojis = []; let mentionedUsers = []; @@ -532,6 +545,9 @@ export default define(meta, paramDef, async (ps, user) => { if (ps.text !== note.text) { update.text = ps.text; } + if (ps.lang !== note.lang) { + update.lang = ps.lang; + } if (ps.cw !== note.cw || (ps.cw && !note.cw)) { update.cw = ps.cw; } diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index 7f28f443f..52078515a 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -492,7 +492,7 @@ router.get("/notes/:note", async (ctx, next) => { ctx.set("Cache-Control", "public, max-age=15"); ctx.set( "Content-Security-Policy", - "default-src 'self' 'unsafe-inline'; img-src *; frame-ancestors *", + "default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src *; font-src 'self' data:; img-src *; media-src *; worker-src 'self'; frame-ancestors *", ); return; diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 5ed711f4c..85c85cbd5 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -61,6 +61,8 @@ import { shouldSilenceInstance } from "@/misc/should-block-instance.js"; import meilisearch from "../../db/meilisearch.js"; import { redisClient } from "@/db/redis.js"; import { Mutex } from "redis-semaphore"; +import { detect as detectLanguage } from "tinyld"; +import { langmap } from "@/misc/langmap.js"; const mutedWordsCache = new Cache< { userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[] @@ -133,6 +135,7 @@ type Option = { createdAt?: Date | null; name?: string | null; text?: string | null; + lang?: string | null; reply?: Note | null; renote?: Note | null; files?: DriveFile[] | null; @@ -270,6 +273,16 @@ export default async ( data.text = null; } + if (data.lang) { + if (!Object.keys(langmap).includes(data.lang.trim())) + throw new Error("invalid param"); + data.lang = data.lang.trim().split("-")[0].split("@")[0]; + } else if (data.text) { + data.lang = detectLanguage(data.text); + } else { + data.lang = null; + } + let tags = data.apHashtags; let emojis = data.apEmojis; let mentionedUsers = data.apMentions; @@ -699,6 +712,7 @@ async function insertNote( : null, name: data.name, text: data.text, + lang: data.lang, hasPoll: data.poll != null, cw: data.cw == null ? null : data.cw, tags: tags.map((tag) => normalizeForSearch(tag)), diff --git a/packages/client/package.json b/packages/client/package.json index 5d4d2ef78..072ee6c1a 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -60,6 +60,7 @@ "insert-text-at-cursor": "0.3.0", "json5": "2.2.3", "katex": "0.16.8", + "libopenmpt-wasm": "github:TheEssem/libopenmpt-packaging#build", "matter-js": "0.19.0", "mfm-js": "0.23.3", "photoswipe": "5.3.9", diff --git a/packages/client/src/components/MkInstanceTicker.vue b/packages/client/src/components/MkInstanceTicker.vue index 97dbaff3f..1fe55f8e5 100644 --- a/packages/client/src/components/MkInstanceTicker.vue +++ b/packages/client/src/components/MkInstanceTicker.vue @@ -6,7 +6,6 @@ " class="hpaizdrt" :style="bg" - @click.stop="openServerInfo" > {{ instance.name }} @@ -19,8 +18,6 @@ import { ref } from "vue"; import { instanceName, version } from "@/config"; import { instance as Instance } from "@/instance"; import { getProxiedImageUrlNullable } from "@/scripts/media-proxy"; -import { defaultStore } from "@/store"; -import { pageWindow } from "@/os"; const props = defineProps<{ instance?: { @@ -94,13 +91,6 @@ function getInstanceIcon(instance): string { "/client-assets/dummy.png" ); } - -function openServerInfo() { - if (!defaultStore.state.openServerInfo) return; - const instanceInfoUrl = - props.host == null ? "/about" : `/instance-info/${props.host}`; - pageWindow(instanceInfoUrl); -} diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue index a22f0857f..aec949ba6 100644 --- a/packages/client/src/components/MkNote.vue +++ b/packages/client/src/components/MkNote.vue @@ -97,7 +97,11 @@
- +
@@ -57,10 +58,12 @@ import MkInstanceTicker from "@/components/MkInstanceTicker.vue"; import { notePage } from "@/filters/note"; import { userPage } from "@/filters/user"; import { i18n } from "@/i18n"; +import { pageWindow } from "@/os"; const props = defineProps<{ note: misskey.entities.Note; pinned?: boolean; + canOpenServerInfo?: boolean; }>(); const note = ref(props.note); @@ -69,6 +72,19 @@ const showTicker = defaultStore.state.instanceTicker === "always" || (defaultStore.state.instanceTicker === "remote" && note.value.user.instance); + +function openServerInfo() { + if ( + (props.canOpenServerInfo && !defaultStore.state.openServerInfo) || + !note.value.user.instance + ) + return; + const instanceInfoUrl = + props.host == null + ? "/about" + : `/instance-info/${note.value.user.instance}`; + pageWindow(instanceInfoUrl); +}