diff --git a/CHANGELOG.md b/CHANGELOG.md index 08fb008e54..0c3ad43141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,18 @@ ## 2023.x.x (unreleased) +### General +- Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed) +- Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83) + +### Client +- + +### Server +- + +## 2023.11.1 + ### General - Feat: 管理者がコントロールパネルからメールアドレスの照会を行えるようになりました - Enhance: ローカリゼーションの更新 @@ -21,6 +33,10 @@ - Enhance: json-schema(OpenAPIの戻り値として使用されるスキーマ定義)を出来る限り最新化 #12311 ### Client +- Enhance: MFMでルビを振れるように + - 例: `$[ruby 三須木 みすき]` +- Enhance: MFMでUNIX時間を指定して日時を表示できるように + - 例: `$[unixtime 1701356400]` - Enhance: プラグインでエラーが発生した場合のハンドリングを強化 - Enhance: 細かなUIのブラッシュアップ - Fix: 効果音が再生されるとデバイスで再生している動画や音声が停止する問題を修正 #12339 diff --git a/locales/en-US.yml b/locales/en-US.yml index 09fd726c9f..2aba028e45 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -564,6 +564,10 @@ output: "Output" script: "Script" disablePagesScript: "Disable AiScript on Pages" updateRemoteUser: "Update remote user information" +deleteUserAvatar: "Delete user icon" +deleteUserAvatarConfirm: "Are you sure that you want to delete this user's icon?" +deleteUserBanner: "Delete user banner" +deleteUserBannerConfirm: "Are you sure that you want to delete this user's banner?" deleteAllFiles: "Delete all files" deleteAllFilesConfirm: "Are you sure that you want to delete all files?" removeAllFollowing: "Unfollow all followed users" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 79ce76c5ea..94254fd998 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -764,7 +764,7 @@ inUse: "utilisé" editCode: "Modifier le code" apply: "Appliquer" receiveAnnouncementFromInstance: "Recevoir les messages d'information de l'instance" -emailNotification: "Notifications par mail" +emailNotification: "Notifications par courriel" publish: "Public" inChannelSearch: "Chercher dans le canal" useReactionPickerForContextMenu: "Clic-droit pour ouvrir le panneau de réactions" @@ -998,6 +998,7 @@ license: "Licence" myClips: "Mes clips" retryAllQueuesConfirmText: "Cela peut augmenter temporairement la charge du serveur." showClipButtonInNoteFooter: "Ajouter « Clip » au menu d'action de la note" +reactionsDisplaySize: "Taille de l'affichage des réactions" noteIdOrUrl: "Identifiant de la note ou URL" video: "Vidéo" videos: "Vidéos" @@ -1053,6 +1054,7 @@ pastAnnouncements: "Annonces passées" replies: "Répondre" renotes: "Renoter" loadReplies: "Inclure les réponses" +loadConversation: "Afficher la conversation" pinnedList: "Liste épinglée" notifyNotes: "Notifier à propos des nouvelles notes" authentication: "Authentification" @@ -1144,7 +1146,7 @@ _initialTutorial: direct: "Uniquement visible aux utilisateurs de votre choix. Les récipients seront notifiés. Cette option peut être utilisée comme alternative aux messages directs." doNotSendConfidencialOnDirect1: "Faites attention quand vous envoyez vos informations sensibles !" doNotSendConfidencialOnDirect2: "Les administrateurs de l'instance destinataire peuvent voir toutes les notes publiées. Soyez prudent·e avec vos informations sensibles quand vous envoyez des notes directes aux utilisateurs dont vous ne vous fiez pas aux instances." - localOnly: "Désactiver la fédération de la note à d'autres instances. Les utilisateurs d'autres instances ne pourront pas voir directement la note quelle que soit l'étendue de la publication mentionnée ci-dessus." + localOnly: "Désactiver la fédération de la note aux autres instances. Les utilisateurs des autres instances ne pourront pas voir directement la note quelle que soit l'étendue de la publication mentionnée ci-dessus." _cw: title: "Masquer le contenu (CW)" description: "Au lieu du corps du texte, le contenu du champ « commentaires » s'affichera. Appuyez sur « afficher le contenu » pour voir le corps du texte." @@ -1171,7 +1173,12 @@ _timelineDescription: global: "Sur le fil global, vous pouvez voir les notes de toutes les instances connectées." _serverSettings: iconUrl: "URL de l’icône" + appIconResolutionMustBe: "La résolution doit être au moins {resolution}." + shortName: "Nom court" + shortNameDescription: "Si le nom officiel de l'instance est long, cette abréviation peut être affichée à la place." fanoutTimelineDescription: "Si activée, la performance de la récupération de la chronologie augmentera considérablement et la charge sur la base de données sera réduite. En revanche, l'utilisation de la mémoire de Redis augmentera. Considérez désactiver cette option si le serveur est bas en mémoire ou instable." + fanoutTimelineDbFallback: "Recours à la base de données" + fanoutTimelineDbFallbackDescription: "Si activée, une demande supplémentaire à la base de données est effectuée comme solution de rechange quand le fil n'est pas mis en cache. Si désactivée, la demande à la base de données n'est pas effectuée, ce qui réduit davantage la charge du serveur mais limite l'étendue du fil récupérable." _accountMigration: moveFrom: "Migrer un autre compte vers le présent compte" moveFromSub: "Créer un alias vers un autre compte" @@ -1304,6 +1311,9 @@ _achievements: flavor: "Attendez une minute, vous êtes sur le mauvais site web ?" _brainDiver: flavor: "Misskey-Misskey La-Tu-Ma" + _smashTestNotificationButton: + title: "Débordement de tests" + description: "Détruire le bouton de test de notifications dans un intervalle extrêmement court" _tutorialCompleted: title: "Diplôme de la course élémentaire de Misskey" description: "Terminer le tutoriel" @@ -1332,6 +1342,7 @@ _role: canManageCustomEmojis: "Gestion des émojis personnalisés" canManageAvatarDecorations: "Gestion des décorations d'avatar" wordMuteMax: "Nombre maximal de caractères dans le filtre de mots" + canUseTranslator: "Usage de la fonctionnalité de traduction" _sensitiveMediaDetection: description: "L'apprentissage automatique peut être utilisé pour détecter automatiquement les médias sensibles à modérer. La sollicitation des serveurs augmente légèrement." sensitivity: "Sensibilité de la détection" @@ -1819,6 +1830,7 @@ _notification: unreadAntennaNote: "Antenne {name}" emptyPushNotificationMessage: "Les notifications push ont été mises à jour" achievementEarned: "Accomplissement" + testNotification: "Tester la notification" reactedBySomeUsers: "{n} utilisateur·rice·s ont réagi" renotedBySomeUsers: "{n} utilisateur·rice·s ont renoté" followedBySomeUsers: "{n} utilisateur·rice·s se sont abonné·e·s à vous" diff --git a/locales/index.d.ts b/locales/index.d.ts index 968334e31b..6fd6d3641a 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -567,6 +567,10 @@ export interface Locale { "script": string; "disablePagesScript": string; "updateRemoteUser": string; + "deleteUserAvatar": string; + "deleteUserAvatarConfirm": string; + "deleteUserBanner": string; + "deleteUserBannerConfirm": string; "deleteAllFiles": string; "deleteAllFilesConfirm": string; "removeAllFollowing": string; @@ -1948,6 +1952,15 @@ export interface Locale { "yearsAgo": string; "invalid": string; }; + "_timeIn": { + "seconds": string; + "minutes": string; + "hours": string; + "days": string; + "weeks": string; + "months": string; + "years": string; + }; "_time": { "second": string; "minute": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 5b1d0d62bd..9685e9c5a5 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -564,6 +564,10 @@ output: "出力" script: "スクリプト" disablePagesScript: "Pagesのスクリプトを無効にする" updateRemoteUser: "リモートユーザー情報の更新" +deleteUserAvatar: "アイコンを削除" +deleteUserAvatarConfirm: "アイコンを削除しますか?" +deleteUserBanner: "バナーを削除" +deleteUserBannerConfirm: "バナーを削除しますか?" deleteAllFiles: "すべてのファイルを削除" deleteAllFilesConfirm: "すべてのファイルを削除しますか?" removeAllFollowing: "フォローを全解除" @@ -1853,6 +1857,15 @@ _ago: yearsAgo: "{n}年前" invalid: "ありません" +_timeIn: + seconds: "{n}秒後" + minutes: "{n}分後" + hours: "{n}時間後" + days: "{n}日後" + weeks: "{n}週間後" + months: "{n}ヶ月後" + years: "{n}年後" + _time: second: "秒" minute: "分" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 0361eaf076..d8f7fe5193 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -59,7 +59,7 @@ copyFileId: "Скопировать ID файла" copyFolderId: "Скопировать ID папки" copyProfileUrl: "Скопировать URL профиля " searchUser: "Поиск людей" -reply: "Ответить" +reply: "Ответ" loadMore: "Показать еще" showMore: "Показать еще" showLess: "Закрыть" @@ -1069,7 +1069,7 @@ unused: "Неиспользуемый" expired: "Срок действия приглашения истёк" doYouAgree: "Согласны?" icon: "Аватар" -replies: "Ответить" +replies: "Ответы" renotes: "Репост" flip: "Переворот" _initialAccountSetting: @@ -1899,7 +1899,7 @@ _notification: app: "Уведомления из приложений" _actions: followBack: "отвечает взаимной подпиской" - reply: "Ответить" + reply: "Ответ" renote: "Репост" _deck: alwaysShowMainColumn: "Всегда показывать главную колонку" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index f58fd1e72b..ad0741693f 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -299,7 +299,7 @@ light: "淺色" dark: "深色" lightThemes: "淺色主題" darkThemes: "深色主題" -syncDeviceDarkMode: "同步至此裝置的深色模式設定" +syncDeviceDarkMode: "與設備的深色模式同步" drive: "雲端硬碟" fileName: "檔案名稱" selectFile: "選擇檔案" @@ -1266,6 +1266,8 @@ _serverSettings: shortName: "簡稱" shortNameDescription: "如果伺服器的正式名稱很長,可用簡稱或通稱代替。" fanoutTimelineDescription: "如果啟用的話,檢索各個時間軸的性能會顯著提昇,資料庫的負荷也會減少。不過,Redis 的記憶體使用量會增加。如果伺服器的記憶體容量比較少或者運行不穩定,可以停用。" + fanoutTimelineDbFallback: "資料庫的回退" + fanoutTimelineDbFallbackDescription: "若啟用,在時間軸沒有快取的情況下將執行回退處理以額外查詢資料庫。若停用,可以透過不執行回退處理來進一步減少伺服器的負荷,但會限制可取得的時間軸範圍。" _accountMigration: moveFrom: "從其他帳戶遷移到這個帳戶" moveFromSub: "為另一個帳戶建立別名" @@ -1817,6 +1819,14 @@ _ago: monthsAgo: "{n} 個月前" yearsAgo: "{n} 年前" invalid: "無" +_timeIn: + seconds: "{n} 秒後" + minutes: "{n} 分後" + hours: "{n} 小時後" + days: "{n} 日後" + weeks: "{n} 週後" + months: "{n} 個月後" + years: "{n} 年後" _time: second: "秒" minute: "分鐘" diff --git a/package.json b/package.json index 66cd7fb706..d0fc867b3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2023.11.1-beta.1", + "version": "2023.11.1", "codename": "nasubi", "repository": { "type": "git", @@ -55,9 +55,9 @@ "@typescript-eslint/eslint-plugin": "6.11.0", "@typescript-eslint/parser": "6.11.0", "cross-env": "7.0.3", - "cypress": "13.5.0", + "cypress": "13.5.1", "eslint": "8.53.0", - "start-server-and-test": "2.0.2" + "start-server-and-test": "2.0.3" }, "optionalDependencies": { "@tensorflow/tfjs-core": "4.4.0" diff --git a/packages/backend/migration/1700303245007-supportVerifyMailApi.js b/packages/backend/migration/1700303245007-supportVerifyMailApi.js new file mode 100644 index 0000000000..3ac59ec37a --- /dev/null +++ b/packages/backend/migration/1700303245007-supportVerifyMailApi.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class SupportVerifyMailApi1700303245007 { + name = 'SupportVerifyMailApi1700303245007' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "verifymailAuthKey" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "enableVerifymailApi" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableVerifymailApi"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "verifymailAuthKey"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index beb9661fa1..a4856709c3 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -64,7 +64,7 @@ "@bull-board/ui": "5.9.1", "@discordapp/twemoji": "14.1.2", "@fastify/accepts": "4.2.0", - "@fastify/cookie": "9.1.0", + "@fastify/cookie": "9.2.0", "@fastify/cors": "8.4.1", "@fastify/express": "2.3.0", "@fastify/http-proxy": "9.3.0", @@ -78,7 +78,7 @@ "@simplewebauthn/server": "8.3.5", "@sinonjs/fake-timers": "11.2.2", "@smithy/node-http-handler": "2.1.5", - "@swc/cli": "0.1.62", + "@swc/cli": "0.1.63", "@swc/core": "1.3.96", "accepts": "1.3.8", "ajv": "8.12.0", @@ -87,7 +87,7 @@ "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.2", - "bullmq": "4.13.2", + "bullmq": "4.13.3", "cacheable-lookup": "7.0.0", "cbor": "9.0.1", "chalk": "5.3.0", @@ -99,7 +99,7 @@ "date-fns": "2.30.0", "deep-email-validator": "0.1.21", "fastify": "4.24.3", - "fastify-raw-body": "^4.2.2", + "fastify-raw-body": "4.3.0", "feed": "4.2.2", "file-type": "18.7.0", "fluent-ffmpeg": "2.1.2", @@ -132,7 +132,7 @@ "oauth2orize": "1.12.0", "oauth2orize-pkce": "0.1.2", "os-utils": "0.0.14", - "otpauth": "9.1.5", + "otpauth": "9.2.0", "parse5": "7.1.2", "pg": "8.11.3", "pkce-challenge": "4.0.1", @@ -144,14 +144,14 @@ "qrcode": "1.5.3", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.20.5", + "re2": "1.20.8", "redis-lock": "0.1.4", "reflect-metadata": "0.1.13", "rename": "1.0.4", "rss-parser": "3.13.0", "rxjs": "7.8.1", "sanitize-html": "2.11.0", - "secure-json-parse": "^2.4.0", + "secure-json-parse": "2.7.0", "sharp": "0.32.6", "sharp-read-bmp": "github:misskey-dev/sharp-read-bmp", "slacc": "0.0.10", @@ -192,7 +192,7 @@ "@types/jsrsasign": "10.5.12", "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", - "@types/node": "20.9.0", + "@types/node": "20.9.1", "@types/node-fetch": "3.0.3", "@types/nodemailer": "6.4.14", "@types/oauth": "0.9.4", diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index c9da3f77c0..f31cec2b3a 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -3,9 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { URLSearchParams } from 'node:url'; import * as nodemailer from 'nodemailer'; import { Inject, Injectable } from '@nestjs/common'; import { validate as validateEmail } from 'deep-email-validator'; +import { SubOutputFormat } from 'deep-email-validator/dist/output/output.js'; import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; @@ -13,6 +15,7 @@ import type Logger from '@/logger.js'; import type { UserProfilesRepository } from '@/models/_.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; @Injectable() export class EmailService { @@ -27,6 +30,7 @@ export class EmailService { private metaService: MetaService, private loggerService: LoggerService, + private httpRequestService: HttpRequestService, ) { this.logger = this.loggerService.getLogger('email'); } @@ -160,14 +164,25 @@ export class EmailService { email: emailAddress, }); - const validated = meta.enableActiveEmailValidation ? await validateEmail({ - email: emailAddress, - validateRegex: true, - validateMx: true, - validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので - validateDisposable: true, // 捨てアドかどうかチェック - validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので - }) : { valid: true, reason: null }; + const verifymailApi = meta.enableVerifymailApi && meta.verifymailAuthKey != null; + let validated; + + if (meta.enableActiveEmailValidation && meta.verifymailAuthKey) { + if (verifymailApi) { + validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey); + } else { + validated = meta.enableActiveEmailValidation ? await validateEmail({ + email: emailAddress, + validateRegex: true, + validateMx: true, + validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので + validateDisposable: true, // 捨てアドかどうかチェック + validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので + }) : { valid: true, reason: null }; + } + } else { + validated = { valid: true, reason: null }; + } const available = exist === 0 && validated.valid; @@ -182,4 +197,65 @@ export class EmailService { null, }; } + + private async verifyMail(emailAddress: string, verifymailAuthKey: string): Promise<{ + valid: boolean; + reason: 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | null; + }> { + const endpoint = 'https://verifymail.io/api/' + emailAddress + '?key=' + verifymailAuthKey; + const res = await this.httpRequestService.send(endpoint, { + method: 'GET', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json, */*', + }, + }); + + const json = (await res.json()) as { + block: boolean; + catch_all: boolean; + deliverable_email: boolean; + disposable: boolean; + domain: string; + email_address: string; + email_provider: string; + mx: boolean; + mx_fallback: boolean; + mx_host: string[]; + mx_ip: string[]; + mx_priority: { [key: string]: number }; + privacy: boolean; + related_domains: string[]; + }; + + if (json.email_address === undefined) { + return { + valid: false, + reason: 'format', + }; + } + if (json.deliverable_email !== undefined && !json.deliverable_email) { + return { + valid: false, + reason: 'smtp', + }; + } + if (json.disposable) { + return { + valid: false, + reason: 'disposable', + }; + } + if (json.mx !== undefined && !json.mx) { + return { + valid: false, + reason: 'mx', + }; + } + + return { + valid: true, + reason: null, + }; + } } diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index b275d1b142..af602168d4 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -276,9 +276,18 @@ export class MfmService { }, fn: (node) => { - const el = doc.createElement('i'); - appendChildren(node.children, el); - return el; + if (node.props.name === 'unixtime') { + const text = node.children[0]!.type === 'text' ? node.children[0].props.text : ''; + const date = new Date(parseInt(text, 10) * 1000); + const el = doc.createElement('time'); + el.setAttribute('datetime', date.toISOString()); + el.textContent = date.toISOString(); + return el; + } else { + const el = doc.createElement('i'); + appendChildren(node.children, el); + return el; + } }, blockCode: (node) => { diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 14a72add1d..83e8962f5d 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -446,6 +446,17 @@ export class MiMeta { }) public enableActiveEmailValidation: boolean; + @Column('boolean', { + default: false, + }) + public enableVerifymailApi: boolean; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public verifymailAuthKey: string | null; + @Column('boolean', { default: true, }) diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 6d813ae04e..3797b46d04 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -24,6 +24,8 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js'; import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js'; import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; +import * as ep___admin_deleteUserAvatar from './endpoints/admin/delete-user-avatar.js'; +import * as ep___admin_deleteUserBanner from './endpoints/admin/delete-user-banner.js'; import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; @@ -383,6 +385,8 @@ const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-de const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default }; const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default }; const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default }; +const $admin_deleteUserAvatar: Provider = { provide: 'ep:admin/delete-user-avatar', useClass: ep___admin_deleteUserAvatar.default }; +const $admin_deleteUserBanner: Provider = { provide: 'ep:admin/delete-user-banner', useClass: ep___admin_deleteUserBanner.default }; const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default }; const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default }; const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default }; @@ -746,6 +750,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_avatarDecorations_list, $admin_avatarDecorations_update, $admin_deleteAllFilesOfAUser, + $admin_deleteUserAvatar, + $admin_deleteUserBanner, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, $admin_drive_files, @@ -1103,6 +1109,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_avatarDecorations_list, $admin_avatarDecorations_update, $admin_deleteAllFilesOfAUser, + $admin_deleteUserAvatar, + $admin_deleteUserBanner, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, $admin_drive_files, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index ef4bd3e2b5..4162ace337 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -24,6 +24,8 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js'; import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js'; import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; +import * as ep___admin_deleteUserAvatar from './endpoints/admin/delete-user-avatar.js'; +import * as ep___admin_deleteUserBanner from './endpoints/admin/delete-user-banner.js'; import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; @@ -381,6 +383,8 @@ const eps = [ ['admin/avatar-decorations/list', ep___admin_avatarDecorations_list], ['admin/avatar-decorations/update', ep___admin_avatarDecorations_update], ['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser], + ['admin/delete-user-avatar', ep___admin_deleteUserAvatar], + ['admin/delete-user-banner', ep___admin_deleteUserBanner], ['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles], ['admin/drive/cleanup', ep___admin_drive_cleanup], ['admin/drive/files', ep___admin_drive_files], diff --git a/packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts b/packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts new file mode 100644 index 0000000000..d3c78d7fb6 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + + if (user == null) { + throw new Error('user not found'); + } + + await this.usersRepository.update(user.id, { + avatar: null, + avatarId: null, + avatarUrl: null, + avatarBlurhash: null, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts b/packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts new file mode 100644 index 0000000000..e076cdcfc1 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + + if (user == null) { + throw new Error('user not found'); + } + + await this.usersRepository.update(user.id, { + banner: null, + bannerId: null, + bannerUrl: null, + bannerBlurhash: null, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index da3e5dd9ac..d6f9b2cd94 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -113,6 +113,8 @@ export const paramDef = { objectStorageS3ForcePathStyle: { type: 'boolean' }, enableIpLogging: { type: 'boolean' }, enableActiveEmailValidation: { type: 'boolean' }, + enableVerifymailApi: { type: 'boolean' }, + verifymailAuthKey: { type: 'string', nullable: true }, enableChartsForRemoteUser: { type: 'boolean' }, enableChartsForFederatedInstances: { type: 'boolean' }, enableServerMachineStats: { type: 'boolean' }, @@ -454,6 +456,18 @@ export default class extends Endpoint { // eslint- set.enableActiveEmailValidation = ps.enableActiveEmailValidation; } + if (ps.enableVerifymailApi !== undefined) { + set.enableVerifymailApi = ps.enableVerifymailApi; + } + + if (ps.verifymailAuthKey !== undefined) { + if (ps.verifymailAuthKey === '') { + set.verifymailAuthKey = null; + } else { + set.verifymailAuthKey = ps.verifymailAuthKey; + } + } + if (ps.enableChartsForRemoteUser !== undefined) { set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser; } diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 4acce1535d..62192d0dab 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -24,8 +24,8 @@ "@rollup/pluginutils": "5.0.5", "@syuilo/aiscript": "0.16.0", "@tabler/icons-webfont": "2.37.0", - "@vitejs/plugin-vue": "4.4.1", - "@vue-macros/reactivity-transform": "0.3.23", + "@vitejs/plugin-vue": "4.5.0", + "@vue-macros/reactivity-transform": "0.4.0", "@vue/compiler-sfc": "3.3.8", "astring": "1.8.6", "autosize": "6.0.1", @@ -57,7 +57,7 @@ "photoswipe": "5.4.2", "punycode": "2.3.1", "querystring": "0.2.1", - "rollup": "4.4.0", + "rollup": "4.4.1", "sanitize-html": "2.11.0", "shiki": "^0.14.5", "sass": "1.69.5", @@ -101,7 +101,7 @@ "@types/estree": "1.0.5", "@types/matter-js": "0.19.4", "@types/micromatch": "4.0.5", - "@types/node": "20.9.0", + "@types/node": "20.9.1", "@types/punycode": "2.1.2", "@types/sanitize-html": "2.9.4", "@types/throttle-debounce": "5.0.2", @@ -115,7 +115,7 @@ "@vue/runtime-core": "3.3.8", "acorn": "8.11.2", "cross-env": "7.0.3", - "cypress": "13.5.0", + "cypress": "13.5.1", "eslint": "8.53.0", "eslint-plugin-import": "2.29.0", "eslint-plugin-vue": "9.18.1", @@ -128,7 +128,7 @@ "prettier": "3.1.0", "react": "18.2.0", "react-dom": "18.2.0", - "start-server-and-test": "2.0.2", + "start-server-and-test": "2.0.3", "storybook": "7.5.3", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "summaly": "github:misskey-dev/summaly", diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 7c4f910559..7e0c219045 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -45,12 +45,12 @@ import contains from '@/scripts/contains.js'; import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js'; import { acct } from '@/filters/user.js'; import * as os from '@/os.js'; -import { MFM_TAGS } from '@/scripts/mfm-tags.js'; import { defaultStore } from '@/store.js'; import { emojilist, getEmojiName } from '@/scripts/emojilist.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; +import { MFM_TAGS } from '@/const.js'; type EmojiDef = { emoji: string; diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts index d14a1fb63c..c5f247bce9 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -7,6 +7,7 @@ import { VNode, h } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import MkUrl from '@/components/global/MkUrl.vue'; +import MkTime from '@/components/global/MkTime.vue'; import MkLink from '@/components/MkLink.vue'; import MkMention from '@/components/MkMention.vue'; import MkEmoji from '@/components/global/MkEmoji.vue'; @@ -238,6 +239,34 @@ export default function(props: MfmProps) { style = `background-color: #${color};`; break; } + case 'ruby': { + if (token.children.length === 1) { + const child = token.children[0]; + const text = child.type === 'text' ? child.props.text : ''; + return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]); + } else { + const rt = token.children.at(-1)!; + const text = rt.type === 'text' ? rt.props.text : ''; + return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]); + } + } + case 'unixtime': { + const child = token.children[0]; + const unixtime = parseInt(child.type === 'text' ? child.props.text : ''); + return h('span', { + style: 'display: inline-block; font-size: 90%; border: solid 1px var(--divider); border-radius: 999px; padding: 4px 10px 4px 6px;', + }, [ + h('i', { + class: 'ti ti-clock', + style: 'margin-right: 0.25em;', + }), + h(MkTime, { + key: Math.random(), + time: unixtime * 1000, + mode: 'detail', + }), + ]); + } } if (style == null) { return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']); diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue index 5ba13ca3f3..f08d538fc0 100644 --- a/packages/frontend/src/components/global/MkTime.vue +++ b/packages/frontend/src/components/global/MkTime.vue @@ -49,8 +49,15 @@ const relative = $computed(() => { ago >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago / 3600).toString() }) : ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) : ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) : - ago >= -1 ? i18n.ts._ago.justNow : - i18n.ts._ago.future); + ago >= -3 ? i18n.ts._ago.justNow : + ago < -31536000 ? i18n.t('_timeIn.years', { n: Math.round(-ago / 31536000).toString() }) : + ago < -2592000 ? i18n.t('_timeIn.months', { n: Math.round(-ago / 2592000).toString() }) : + ago < -604800 ? i18n.t('_timeIn.weeks', { n: Math.round(-ago / 604800).toString() }) : + ago < -86400 ? i18n.t('_timeIn.days', { n: Math.round(-ago / 86400).toString() }) : + ago < -3600 ? i18n.t('_timeIn.hours', { n: Math.round(-ago / 3600).toString() }) : + ago < -60 ? i18n.t('_timeIn.minutes', { n: (~~(-ago / 60)).toString() }) : + i18n.t('_timeIn.seconds', { n: (~~(-ago % 60)).toString() }) + ); }); let tickId: number; diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts index b3071fd924..397f804822 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -92,3 +92,5 @@ export const CURRENT_STICKY_BOTTOM = 'CURRENT_STICKY_BOTTOM'; export const DEFAULT_SERVER_ERROR_IMAGE_URL = 'https://xn--931a.moe/assets/error.jpg'; export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://xn--931a.moe/assets/not-found.jpg'; export const DEFAULT_INFO_IMAGE_URL = 'https://xn--931a.moe/assets/info.jpg'; + +export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime']; diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 5d671acf31..9f4975e888 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -122,6 +122,10 @@ SPDX-License-Identifier: AGPL-3.0-only +
+ {{ i18n.ts.deleteUserAvatar }} + {{ i18n.ts.deleteUserBanner }} +
{{ i18n.ts.deleteAccount }} @@ -320,6 +324,44 @@ async function toggleSuspend(v) { } } +async function deleteUserAvatar() { + const confirm = await os.confirm({ + type: 'warning', + text: i18n.ts.deleteUserAvatarConfirm, + }); + if (confirm.canceled) return; + const process = async () => { + await os.api('admin/delete-user-avatar', { userId: user.id }); + os.success(); + }; + await process().catch(err => { + os.alert({ + type: 'error', + text: err.toString(), + }); + }); + refreshUser(); +} + +async function deleteUserBanner() { + const confirm = await os.confirm({ + type: 'warning', + text: i18n.ts.deleteUserBannerConfirm, + }); + if (confirm.canceled) return; + const process = async () => { + await os.api('admin/delete-user-banner', { userId: user.id }); + os.success(); + }; + await process().catch(err => { + os.alert({ + type: 'error', + text: err.toString(), + }); + }); + refreshUser(); +} + async function deleteAllFiles() { const confirm = await os.confirm({ type: 'warning', diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue index e88860166c..e614bfeb1b 100644 --- a/packages/frontend/src/pages/admin/external-services.vue +++ b/packages/frontend/src/pages/admin/external-services.vue @@ -38,6 +38,7 @@ import { } from 'vue'; import XHeader from './_header_.vue'; import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; import FormSuspense from '@/components/form/suspense.vue'; import FormSection from '@/components/form/section.vue'; import * as os from '@/os.js'; diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue index a2594ee6c5..f7f76d910a 100644 --- a/packages/frontend/src/pages/admin/security.vue +++ b/packages/frontend/src/pages/admin/security.vue @@ -73,6 +73,13 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + + + @@ -132,6 +139,8 @@ let setSensitiveFlagAutomatically: boolean = $ref(false); let enableSensitiveMediaDetectionForVideos: boolean = $ref(false); let enableIpLogging: boolean = $ref(false); let enableActiveEmailValidation: boolean = $ref(false); +let enableVerifymailApi: boolean = $ref(false); +let verifymailAuthKey: string | null = $ref(null); async function init() { const meta = await os.api('admin/meta'); @@ -150,6 +159,8 @@ async function init() { enableSensitiveMediaDetectionForVideos = meta.enableSensitiveMediaDetectionForVideos; enableIpLogging = meta.enableIpLogging; enableActiveEmailValidation = meta.enableActiveEmailValidation; + enableVerifymailApi = meta.enableVerifymailApi; + verifymailAuthKey = meta.verifymailAuthKey; } function save() { @@ -167,6 +178,8 @@ function save() { enableSensitiveMediaDetectionForVideos, enableIpLogging, enableActiveEmailValidation, + enableVerifymailApi, + verifymailAuthKey, }).then(() => { fetchInstance(); }); diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue index c968850444..1e3db42758 100644 --- a/packages/frontend/src/pages/role.vue +++ b/packages/frontend/src/pages/role.vue @@ -18,16 +18,16 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ role.description }}
- -
+ +
{{ i18n.ts.nothing }}
- -
+ +
{{ i18n.ts.nothing }}
@@ -55,7 +55,7 @@ const props = withDefaults(defineProps<{ let tab = $ref(props.initialTab); let role = $ref(); let error = $ref(); -let visiable = $ref(false); +let visible = $ref(false); watch(() => props.role, () => { os.api('roles/show', { @@ -63,7 +63,7 @@ watch(() => props.role, () => { }).then(res => { role = res; document.title = `${role?.name} | ${instanceName}`; - visiable = res.isExplorable && res.isPublic; + visible = res.isExplorable && res.isPublic; }).catch((err) => { if (err.code === 'NO_SUCH_ROLE') { error = i18n.ts.noRole; diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue index 2222381df6..7b09c6c900 100644 --- a/packages/frontend/src/pages/settings/notifications.vue +++ b/packages/frontend/src/pages/settings/notifications.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +