Compare commits

..

6 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] d5f711fbcc Remove unnecessary i18n fallbacks per maintainer feedback
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-08-20 03:35:26 +00:00
copilot-swe-agent[bot] be96f07718 Remove non-Japanese translations per maintainer request
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-08-20 01:29:31 +00:00
copilot-swe-agent[bot] d66a46c022 Add additional locale support and complete email verification feature
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-08-20 01:06:56 +00:00
copilot-swe-agent[bot] 3a3d7768be Fix infinite redirect by removing server route for email verification
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-08-20 01:02:44 +00:00
copilot-swe-agent[bot] bf1e177885 Add branded email verification page with localization support
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-08-20 01:00:30 +00:00
copilot-swe-agent[bot] caa0e7dba6 Initial plan 2025-08-20 00:52:35 +00:00
25 changed files with 247 additions and 269 deletions
-4
View File
@@ -54,10 +54,6 @@
- Fix: メンションとしての条件を満たしていても、特定の条件(`-`が含まれる場合など)で正しくサジェストされない問題を一部修正
### Server
- Feat: サーバー管理コマンド
- `pnpm cli foo` の形式で実行可能です
- 現在以下のコマンドが利用可能です
- `reset-captcha` - CAPTCHA設定をリセットします
- Enhance: ノートの削除処理の効率化
- Enhance: 全体的なパフォーマンスの向上
- Enhance: 依存ソフトウェアの更新
-6
View File
@@ -1054,7 +1054,6 @@ permissionDeniedError: "Operació no permesa "
permissionDeniedErrorDescription: "Aquest compte no té suficients permisos per dur a terme aquesta acció "
preset: "Predefinit"
selectFromPresets: "Escull des dels predefinits"
custom: "Personalitzat"
achievements: "Assoliments"
gotInvalidResponseError: "Resposta del servidor invàlida "
gotInvalidResponseErrorDescription: "No es pot contactar amb el servidor o potser es troba fora de línia per manteniment. Provar-ho de nou més tard."
@@ -1376,7 +1375,6 @@ safeModeEnabled: "Mode segur activat"
pluginsAreDisabledBecauseSafeMode: "Els afegits no estan activats perquè el mode segur està activat."
customCssIsDisabledBecauseSafeMode: "El CSS personalitzat no s'aplica perquè el mode segur es troba activat."
themeIsDefaultBecauseSafeMode: "El tema predeterminat es farà servir mentre el mode segur estigui activat. Una vegada es desactivi el mode segur es restablirà el tema escollit."
thankYouForTestingBeta: "Gràcies per ajudar-nos a provar la versió beta!"
_order:
newest: "Més recent"
oldest: "Antigues primer"
@@ -1666,9 +1664,6 @@ _serverSettings:
userGeneratedContentsVisibilityForVisitor_description2: "La publicació incondicional de tots els continguts del servidor a internet, incloent-hi els continguts remots rebuts pel servidor, comporta riscos. Això és extremadament important per els espectadors que desconeixen el caràcter descentralitzat dels continguts, ja que poden percebre erroneament els continguts remots com contingut generat per el propi servidor."
restartServerSetupWizardConfirm_title: "Vols tornar a executar l'assistent de configuració inicial del servidor?"
restartServerSetupWizardConfirm_text: "Algunes configuracions actuals seran restablertes."
entrancePageStyle: "Estil de la pàgina d'inici"
showTimelineForVisitor: "Mostrar la línia de temps"
showActivityiesForVisitor: "Mostrar les activitats"
_userGeneratedContentsVisibilityForVisitor:
all: "Tot obert al públic "
localOnly: "Només es publiquen els continguts locals, el contingut remot es manté privat"
@@ -2278,7 +2273,6 @@ _time:
minute: "Minut(s)"
hour: "Hor(a)(es)"
day: "Di(a)(es)"
month: "Mes(os)"
_2fa:
alreadyRegistered: "J has registrat un dispositiu d'autenticació de doble factor."
registerTOTP: "Registrar una aplicació autenticadora"
-2
View File
@@ -1054,7 +1054,6 @@ permissionDeniedError: "Operation denied"
permissionDeniedErrorDescription: "This account does not have the permission to perform this action."
preset: "Preset"
selectFromPresets: "Choose from presets"
custom: "Custom"
achievements: "Achievements"
gotInvalidResponseError: "Invalid server response"
gotInvalidResponseErrorDescription: "The server may be unreachable or undergoing maintenance. Please try again later."
@@ -2274,7 +2273,6 @@ _time:
minute: "Minute(s)"
hour: "Hour(s)"
day: "Day(s)"
month: "Month(s)"
_2fa:
alreadyRegistered: "You have already registered a 2-factor authentication device."
registerTOTP: "Register authenticator app"
+1 -7
View File
@@ -1054,7 +1054,6 @@ permissionDeniedError: "Errore, attività non autorizzata"
permissionDeniedErrorDescription: "Non si dispone dell'autorizzazione per eseguire questa operazione."
preset: "Preimpostato"
selectFromPresets: "Seleziona preimpostato"
custom: "Personalizzato"
achievements: "Conquiste"
gotInvalidResponseError: "Risposta del server non valida"
gotInvalidResponseErrorDescription: "Il server potrebbe essere irraggiungibile o in manutenzione. Riprova più tardi."
@@ -1376,7 +1375,6 @@ safeModeEnabled: "La modalità sicura è attiva"
pluginsAreDisabledBecauseSafeMode: "Tutti i plugin sono disattivati, poiché la modalità sicura è attiva."
customCssIsDisabledBecauseSafeMode: "Il CSS personalizzato non è stato applicato, poiché la modalità sicura è attiva."
themeIsDefaultBecauseSafeMode: "Quando la modalità sicura è attiva, viene utilizzato il tema predefinito. Quando la modalità sicura viene disattivata, il tema torna a essere quello precedente."
thankYouForTestingBeta: "Grazie per la tua collaborazione nella verifica delle versioni beta!"
_order:
newest: "Prima i più recenti"
oldest: "Meno recenti prima"
@@ -1666,9 +1664,6 @@ _serverSettings:
userGeneratedContentsVisibilityForVisitor_description2: "Esistono dei rischi nell'esporre incondizionatamente su internet tutto il contenuto del tuo server, incluso il contenuto remoto ricevuto da altri server. In particolare, occorre prestare attenzione, perché le persone non consapevoli della federazione potrebbero erroneamente credere che il contenuto remoto sia stato invece creato all'interno del proprio server."
restartServerSetupWizardConfirm_title: "Vuoi ripetere la procedura guidata di configurazione iniziale del server?"
restartServerSetupWizardConfirm_text: "Verranno ripristinate alcune tue impostazioni personalizzate."
entrancePageStyle: "Stile della pagina di ingresso"
showTimelineForVisitor: "Mostra la Timeline a visitatori non autenticati"
showActivityiesForVisitor: "Mostra le attività a visitatori non autenticati"
_userGeneratedContentsVisibilityForVisitor:
all: "Tutto pubblico"
localOnly: "Pubblica solo contenuti locali, mantieni privati i contenuti remoti"
@@ -2278,7 +2273,6 @@ _time:
minute: "min"
hour: "ore"
day: "giorni"
month: "Mese"
_2fa:
alreadyRegistered: "La configurazione è stata già completata."
registerTOTP: "Registra una App di autenticazione a due fattori (2FA/MFA)"
@@ -3237,7 +3231,7 @@ _imageEffector:
zoomLinesThreshold: "Limite delle linee zoom"
zoomLinesMaskSize: "Ampiezza del diametro"
zoomLinesBlack: "Bande nere"
drafts: "Bozze"
drafts: "Bozza"
_drafts:
select: "Selezionare bozza"
cannotCreateDraftAnymore: "Hai superato il numero massimo di bozze ammissibili."
+3
View File
@@ -776,6 +776,9 @@ highlightSensitiveMedia: "メディアがセンシティブであることを分
verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。"
notSet: "未設定"
emailVerified: "メールアドレスが確認されました"
emailVerifiedDescription: "メールアドレスの認証が完了しました。ページを閉じてお進みください。"
emailVerificationFailed: "メールアドレスの認証に失敗しました"
emailVerificationFailedDescription: "認証リンクが無効か期限切れです。もう一度お試しください。"
noteFavoritesCount: "お気に入りノートの数"
pageLikesCount: "Pageにいいねした数"
pageLikedCount: "Pageにいいねされた数"
-6
View File
@@ -1054,7 +1054,6 @@ permissionDeniedError: "작업이 거부되었습니다"
permissionDeniedErrorDescription: "이 작업을 수행할 권한이 없습니다."
preset: "프리셋"
selectFromPresets: "프리셋에서 선택"
custom: "커스텀"
achievements: "도전 과제"
gotInvalidResponseError: "서버의 응답이 올바르지 않습니다"
gotInvalidResponseErrorDescription: " 서버가 다운되었거나 점검중일 가능성이 있습니다. 잠시후에 다시 시도해 주십시오."
@@ -1376,7 +1375,6 @@ safeModeEnabled: "세이프 모드가 활성화돼있습니다"
pluginsAreDisabledBecauseSafeMode: "세이프 모드가 활성화돼있기에 플러그인은 전부 비활성화됩니다."
customCssIsDisabledBecauseSafeMode: "세이프 모드가 활성화돼있기에 커스텀 CSS는 적용되지 않습니다."
themeIsDefaultBecauseSafeMode: "세이프 모드가 활성화돼있는 동안에는 기본 테마가 사용됩니다. 세이프 모드를 끄면 원래대로 돌아옵니다."
thankYouForTestingBeta: "베타 버전의 검증에 협력해 주셔서 감사합니다!"
_order:
newest: "최신 순"
oldest: "오래된 순"
@@ -1666,9 +1664,6 @@ _serverSettings:
userGeneratedContentsVisibilityForVisitor_description2: "서버에서 받은 리모트 콘텐츠를 포함해 서버 내의 모든 콘텐츠를 무조건 인터넷에 공개하는 것에는 위험이 따릅니다. 특히, 분산형 특성에 대해 모르는 열람자에게는 리모트 콘텐츠여도 서버 내에서 작성된 콘텐츠라고 잘못 인식할 수 있기에 주의가 필요합니다."
restartServerSetupWizardConfirm_title: "서버의 초기 설정 위자드를 재시도하시겠습니까?"
restartServerSetupWizardConfirm_text: "현재 일부 설정은 리셋됩니다."
entrancePageStyle: "입구 페이지의 스타일"
showTimelineForVisitor: "타임라인 표시"
showActivityiesForVisitor: "활동 표시"
_userGeneratedContentsVisibilityForVisitor:
all: "모두 공개"
localOnly: "로컬 콘텐츠만 공개하고 리모트 콘텐츠는 비공개"
@@ -2278,7 +2273,6 @@ _time:
minute: "분"
hour: "시간"
day: "일"
month: "개월"
_2fa:
alreadyRegistered: "이미 설정이 완료되었습니다."
registerTOTP: "인증 앱 설정 시작"
-3
View File
@@ -1054,7 +1054,6 @@ permissionDeniedError: "การดำเนินถูกปฏิเสธ"
permissionDeniedErrorDescription: "บัญชีนี้ไม่มีสิทธิ์อนุญาตในการดำเนินการนี้"
preset: "พรีเซ็ต"
selectFromPresets: "เลือกจากการพรีเซ็ต"
custom: "แบบกำหนดเอง"
achievements: "ความสำเร็จ"
gotInvalidResponseError: "การตอบสนองเซิร์ฟเวอร์ไม่ถูกต้อง"
gotInvalidResponseErrorDescription: "เซิร์ฟเวอร์อาจไม่สามารถเข้าถึงได้หรืออาจจะกำลังอยู่ในระหว่างปรับปรุง กรุณาลองใหม่อีกครั้งในภายหลังนะคะ"
@@ -1376,7 +1375,6 @@ safeModeEnabled: "โหมดปลอดภัยถูกเปิดใช
pluginsAreDisabledBecauseSafeMode: "เนื่องจากโหมดปลอดภัยถูกเปิดใช้งาน ปลั๊กอินทั้งหมดจึงถูกปิดใช้งาน"
customCssIsDisabledBecauseSafeMode: "เนื่องจากโหมดปลอดภัยถูกเปิดใช้งาน CSS แบบกำหนดเองจึงไม่ได้ถูกนำมาใช้"
themeIsDefaultBecauseSafeMode: "ในระหว่างที่โหมดปลอดภัยถูกเปิดใช้งาน จะใช้ธีมเริ่มต้น เมื่อปิดโหมดปลอดภัยจะกลับคืนดังเดิม"
thankYouForTestingBeta: "ขอบคุณที่ให้ความร่วมมือในการทดสอบเวอร์ชันเบต้า!"
_order:
newest: "เรียงจากใหม่ไปเก่า"
oldest: "เรียงจากเก่าไปใหม่"
@@ -2275,7 +2273,6 @@ _time:
minute: "นาที"
hour: "ชั่วโมง"
day: "วัน"
month: "เดือน"
_2fa:
alreadyRegistered: "คุณได้ลงทะเบียนอุปกรณ์ยืนยันตัวตนแบบ 2 ชั้นแล้ว"
registerTOTP: "ลงทะเบียนแอพตัวตรวจสอบสิทธิ์"
-6
View File
@@ -1054,7 +1054,6 @@ permissionDeniedError: "操作被拒绝"
permissionDeniedErrorDescription: "本账户没有执行该操作的权限。"
preset: "预设值"
selectFromPresets: "从预设值中选择"
custom: "自定义"
achievements: "成就"
gotInvalidResponseError: "服务器无应答"
gotInvalidResponseErrorDescription: "您的网络连接可能出现了问题, 或是远程服务器暂时不可用. 请稍后重试。"
@@ -1376,7 +1375,6 @@ safeModeEnabled: "已启用安全模式"
pluginsAreDisabledBecauseSafeMode: "因启用了安全模式,所有插件均已被禁用。"
customCssIsDisabledBecauseSafeMode: "因启用了安全模式,无法应用自定义 CSS。"
themeIsDefaultBecauseSafeMode: "启用安全模式时将使用默认主题。关闭安全模式后将还原。"
thankYouForTestingBeta: "感谢您协助测试 beta 版!"
_order:
newest: "从新到旧"
oldest: "从旧到新"
@@ -1666,9 +1664,6 @@ _serverSettings:
userGeneratedContentsVisibilityForVisitor_description2: "包含服务器接收到的远程内容在内,无条件将服务器上的所有内容公开在互联网上存在风险。特别是对去中心化的特性不是很了解的访问者有可能将远程服务器上的内容误认为是在此服务器内生成的,需要特别留意。"
restartServerSetupWizardConfirm_title: "要重新开始服务器初始设定向导吗?"
restartServerSetupWizardConfirm_text: "现有的部分设定将重置。"
entrancePageStyle: "入口页面样式"
showTimelineForVisitor: "显示时间线"
showActivityiesForVisitor: "显示活动"
_userGeneratedContentsVisibilityForVisitor:
all: "全部公开"
localOnly: "仅公开本地内容,隐藏远程内容"
@@ -2278,7 +2273,6 @@ _time:
minute: "分"
hour: "小时"
day: "日"
month: "个月"
_2fa:
alreadyRegistered: "此设备已被注册"
registerTOTP: "开始设置验证器"
-6
View File
@@ -1054,7 +1054,6 @@ permissionDeniedError: "操作被拒絕"
permissionDeniedErrorDescription: "此帳戶沒有執行這個操作的權限。"
preset: "預設值"
selectFromPresets: "從預設值中選擇"
custom: "自訂"
achievements: "成就"
gotInvalidResponseError: "伺服器的回應無效"
gotInvalidResponseErrorDescription: "伺服器可能已關閉或者在維護中,請稍後再試。"
@@ -1376,7 +1375,6 @@ safeModeEnabled: "啟用安全模式"
pluginsAreDisabledBecauseSafeMode: "由於啟用安全模式,所有的外掛都被停用。"
customCssIsDisabledBecauseSafeMode: "由於啟用安全模式,所有的客製 CSS 都被停用。"
themeIsDefaultBecauseSafeMode: "在安全模式啟用期間將使用預設主題。關閉安全模式後會恢復原本的設定。"
thankYouForTestingBeta: "感謝您協助驗證 beta 版!"
_order:
newest: "最新的在前"
oldest: "最舊的在前"
@@ -1666,9 +1664,6 @@ _serverSettings:
userGeneratedContentsVisibilityForVisitor_description2: "包括伺服器接收到的遠端內容在內,無條件地將伺服器內所有內容公開到網際網路上是具有風險的。特別是對於不了解分散式架構特性的瀏覽者來說,他們可能會誤以為這些遠端內容是由該伺服器所創建的,因此需要特別留意。"
restartServerSetupWizardConfirm_title: "要重新執行伺服器的初始設定精靈嗎?"
restartServerSetupWizardConfirm_text: "當前的部分設定將會被重設。"
entrancePageStyle: "入口頁面的樣式"
showTimelineForVisitor: "顯示時間軸"
showActivityiesForVisitor: "顯示活動"
_userGeneratedContentsVisibilityForVisitor:
all: "全部公開\n"
localOnly: "僅公開本地內容,遠端內容則不公開\n"
@@ -2278,7 +2273,6 @@ _time:
minute: "分鐘"
hour: "小時"
day: "日"
month: "個月"
_2fa:
alreadyRegistered: "此裝置已被註冊過了"
registerTOTP: "開始設定驗證應用程式"
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2025.8.0-beta.2",
"version": "2025.8.0-beta.1",
"codename": "nasubi",
"repository": {
"type": "git",
@@ -27,7 +27,6 @@
"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js",
"start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
"cli": "cd packages/backend && pnpm cli",
"init": "pnpm migrate",
"migrate": "cd packages/backend && pnpm migrate",
"revert": "cd packages/backend && pnpm revert",
-1
View File
@@ -11,7 +11,6 @@
"start:test": "cross-env NODE_ENV=test node ./built/boot/entry.js",
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
"cli": "node ./built/boot/cli.js",
"check:connect": "node ./scripts/check_connect.js",
"build": "swc src -d built -D --strip-leading-paths",
"build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths",
-49
View File
@@ -1,49 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import 'reflect-metadata';
import { EventEmitter } from 'node:events';
import { NestFactory } from '@nestjs/core';
import { CommandModule } from '@/cli/CommandModule.js';
import { NestLogger } from '@/NestLogger.js';
import { CommandService } from '@/cli/CommandService.js';
process.title = 'Misskey Cli';
Error.stackTraceLimit = Infinity;
EventEmitter.defaultMaxListeners = 128;
const app = await NestFactory.createApplicationContext(CommandModule, {
logger: new NestLogger(),
});
const commandService = app.get(CommandService);
const command = process.argv[2] ?? 'help';
switch (command) {
case 'help': {
console.log('Available commands:');
console.log(' help - Displays this help message');
console.log(' reset-captcha - Resets the captcha');
break;
}
case 'ping': {
await commandService.ping();
break;
}
case 'reset-captcha': {
await commandService.resetCaptcha();
console.log('Captcha has been reset.');
break;
}
default: {
console.error(`Unrecognized command: ${command}`);
console.error('Use "help" to see available commands.');
process.exit(1);
}
}
process.exit(0);
-23
View File
@@ -1,23 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Module } from '@nestjs/common';
import { CoreModule } from '@/core/CoreModule.js';
import { GlobalModule } from '@/GlobalModule.js';
import { CommandService } from './CommandService.js';
@Module({
imports: [
GlobalModule,
CoreModule,
],
providers: [
CommandService,
],
exports: [
CommandService,
],
})
export class CommandModule {}
@@ -1,49 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
@Injectable()
export class CommandService {
private logger: Logger;
constructor(
@Inject(DI.config)
private config: Config,
private metaService: MetaService,
) {
}
@bindThis
public async ping() {
console.log('pong');
}
@bindThis
public async resetCaptcha() {
await this.metaService.update({
enableHcaptcha: false,
hcaptchaSiteKey: null,
hcaptchaSecretKey: null,
enableMcaptcha: false,
mcaptchaSitekey: null,
mcaptchaSecretKey: null,
mcaptchaInstanceUrl: null,
enableRecaptcha: false,
recaptchaSiteKey: null,
recaptchaSecretKey: null,
enableTurnstile: false,
turnstileSiteKey: null,
turnstileSecretKey: null,
enableTestcaptcha: false,
});
}
}
@@ -267,7 +267,7 @@ export class NotificationService implements OnApplicationShutdown {
excludeTypes?: (MiNotification['type'] | string)[],
},
): Promise<MiNotification[]> {
let sinceTime = sinceId && sinceId !== '0' ? this.toXListId(sinceId) : null;
let sinceTime = sinceId ? this.toXListId(sinceId) : null;
let untilTime = untilId ? this.toXListId(untilId) : null;
let notifications: MiNotification[];
@@ -275,11 +275,10 @@ export class NotificationService implements OnApplicationShutdown {
let notificationsRes: [id: string, fields: string[]][];
// sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照
// sinceId が '0' の場合は最初から古い順で取得
if ((sinceTime && !untilTime) || (sinceId === '0' && !untilTime)) {
if (sinceTime && !untilTime) {
notificationsRes = await this.redisClient.xrange(
`notificationTimeline:${userId}`,
sinceTime ? '(' + sinceTime : '-',
'(' + sinceTime,
'+',
'COUNT', limit);
} else {
@@ -308,7 +307,7 @@ export class NotificationService implements OnApplicationShutdown {
}
// フィルタしたことで通知が0件になった場合、次のページを取得する
if ((sinceId && !untilId) || (sinceId === '0' && !untilId)) {
if (sinceId && !untilId) {
sinceTime = notificationsRes[notificationsRes.length - 1][0];
} else {
untilTime = notificationsRes[notificationsRes.length - 1][0];
+1 -23
View File
@@ -238,29 +238,7 @@ export class ServerService implements OnApplicationShutdown {
}
});
fastify.get<{ Params: { code: string } }>('/verify-email/:code', async (request, reply) => {
const profile = await this.userProfilesRepository.findOneBy({
emailVerifyCode: request.params.code,
});
if (profile != null) {
await this.userProfilesRepository.update({ userId: profile.userId }, {
emailVerified: true,
emailVerifyCode: null,
});
this.globalEventService.publishMainStream(profile.userId, 'meUpdated', await this.userEntityService.pack(profile.userId, { id: profile.userId }, {
schema: 'MeDetailed',
includeSecrets: true,
}));
reply.code(200).send('Verification succeeded! メールアドレスの認証に成功しました。');
return;
} else {
reply.code(404).send('Verification failed. Please try again. メールアドレスの認証に失敗しました。もう一度お試しください');
return;
}
});
// Email verification is now handled by frontend route /verify-email/:code
fastify.register(this.clientServerService.createServer);
@@ -382,6 +382,7 @@ export * as 'sw/update-registration' from './endpoints/sw/update-registration.js
export * as 'test' from './endpoints/test.js';
export * as 'username/available' from './endpoints/username/available.js';
export * as 'users' from './endpoints/users.js';
export * as 'verify-email' from './endpoints/verify-email.js';
export * as 'users/achievements' from './endpoints/users/achievements.js';
export * as 'users/clips' from './endpoints/users/clips.js';
export * as 'users/featured-notes' from './endpoints/users/featured-notes.js';
@@ -0,0 +1,78 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ApiError } from '../error.js';
export const meta = {
requireCredential: false,
tags: ['account'],
errors: {
noSuchCode: {
message: 'No such code.',
code: 'NO_SUCH_CODE',
id: '97c1f576-e4b8-4b8a-a6dc-9cb65e7f6f85',
},
},
res: {
type: 'object',
properties: {
success: {
type: 'boolean',
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
code: { type: 'string' },
},
required: ['code'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private userEntityService: UserEntityService,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps) => {
const profile = await this.userProfilesRepository.findOneBy({
emailVerifyCode: ps.code,
});
if (profile == null) {
throw new ApiError(meta.errors.noSuchCode);
}
await this.userProfilesRepository.update({ userId: profile.userId }, {
emailVerified: true,
emailVerifyCode: null,
});
this.globalEventService.publishMainStream(profile.userId, 'meUpdated', await this.userEntityService.pack(profile.userId, { id: profile.userId }, {
schema: 'MeDetailed',
includeSecrets: true,
}));
return {
success: true,
};
});
}
}
@@ -1,62 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { api, post, signup } from '../utils.js';
import type * as misskey from 'misskey-js';
describe('Notification Sorting', () => {
let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
beforeAll(async () => {
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
}, 1000 * 60 * 2);
test('notifications are sorted correctly when oldest first is requested', async () => {
// Create some notifications by having Bob mention Alice
const note1 = await post(bob, { text: '@alice first mention' });
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second
const note2 = await post(bob, { text: '@alice second mention' });
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second
const note3 = await post(bob, { text: '@alice third mention' });
// Test newest first (default)
const newestFirstRes = await api('i/notifications', {
includeTypes: ['mention'],
limit: 10,
}, alice);
assert.strictEqual(newestFirstRes.status, 200);
const newestFirst = newestFirstRes.body.filter(n => n.type === 'mention');
assert.strictEqual(newestFirst.length >= 3, true);
// Verify newest first order (note3 should be first)
const newestFirstIds = newestFirst.map(n => n.note.id);
const note3Index = newestFirstIds.indexOf(note3.id);
const note1Index = newestFirstIds.indexOf(note1.id);
assert.strictEqual(note3Index < note1Index, true);
// Test oldest first with sinceId: '0'
const oldestFirstRes = await api('i/notifications', {
includeTypes: ['mention'],
sinceId: '0',
limit: 10,
}, alice);
assert.strictEqual(oldestFirstRes.status, 200);
const oldestFirst = oldestFirstRes.body.filter(n => n.type === 'mention');
assert.strictEqual(oldestFirst.length >= 3, true);
// Verify oldest first order (note1 should be first)
const oldestFirstIds = oldestFirst.map(n => n.note.id);
const note1IndexOldest = oldestFirstIds.indexOf(note1.id);
const note3IndexOldest = oldestFirstIds.indexOf(note3.id);
assert.strictEqual(note1IndexOldest < note3IndexOldest, true);
});
});
-7
View File
@@ -29,7 +29,6 @@ import { miLocalStorage } from '@/local-storage.js';
import { fetchCustomEmojis } from '@/custom-emojis.js';
import { prefer } from '@/preferences.js';
import { $i } from '@/i.js';
import { launchPlugins } from '@/plugin.js';
export async function common(createVue: () => Promise<App<Element>>) {
console.info(`Misskey v${version}`);
@@ -339,12 +338,6 @@ export async function common(createVue: () => Promise<App<Element>>) {
});
}
try {
await launchPlugins();
} catch (error) {
console.error('Failed to launch plugins:', error);
}
app.mount(rootEl);
// boot.jsのやつを解除
+4 -1
View File
@@ -26,6 +26,7 @@ import { mainRouter } from '@/router.js';
import { makeHotkey } from '@/utility/hotkey.js';
import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js';
import { prefer } from '@/preferences.js';
import { launchPlugins } from '@/plugin.js';
import { updateCurrentAccountPartial } from '@/accounts.js';
import { migrateOldSettings } from '@/pref-migrate.js';
import { unisonReload } from '@/utility/unison-reload.js';
@@ -78,6 +79,8 @@ export async function mainBoot() {
}
}
launchPlugins();
try {
if (prefer.s.enableSeasonalScreenEffect) {
const month = new Date().getMonth() + 1;
@@ -418,7 +421,7 @@ export async function mainBoot() {
}
},
allowRepeat: true,
},
}
} as const satisfies Keymap;
window.document.addEventListener('keydown', makeHotkey(keymap), { passive: false });
@@ -0,0 +1,146 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<PageWithAnimBg>
<div :class="$style.formContainer">
<form :class="$style.form" class="_panel">
<div :class="$style.banner">
<i v-if="verified === true" class="ti ti-check"></i>
<i v-else-if="verified === false" class="ti ti-x"></i>
<i v-else class="ti ti-mail"></i>
</div>
<div class="_gaps_m" style="padding: 32px;">
<!-- Success state -->
<div v-if="verified === true">
<div :class="$style.title">{{ i18n.ts.emailVerified }}</div>
<div>{{ i18n.ts.emailVerifiedDescription }}</div>
<div>
<MkButton gradate large rounded @click="goToLogin" style="margin: 0 auto;">
{{ i18n.ts.gotIt }}
</MkButton>
</div>
</div>
<!-- Error state -->
<div v-else-if="verified === false">
<div :class="$style.title">{{ i18n.ts.emailVerificationFailed }}</div>
<div>{{ i18n.ts.emailVerificationFailedDescription }}</div>
<div>
<MkButton gradate large rounded @click="goHome" style="margin: 0 auto;">
{{ i18n.ts.gotIt }}
</MkButton>
</div>
</div>
<!-- Loading state -->
<div v-else>
<div :class="$style.title">{{ i18n.ts.loading }}</div>
<MkLoading/>
</div>
</div>
</form>
<!-- Instance branding -->
<div :class="$style.branding">
<div :class="$style.poweredBy">Powered by</div>
<img :src="misskeysvg" :class="$style.misskey"/>
</div>
</div>
</PageWithAnimBg>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkLoading from '@/components/MkLoading.vue';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { mainRouter } from '@/router.js';
import misskeysvg from '/client-assets/misskey.svg';
const verified = ref<boolean | null>(null);
const props = defineProps<{
code: string;
}>();
onMounted(async () => {
try {
await misskeyApi('verify-email', {
code: props.code,
});
verified.value = true;
} catch (err) {
verified.value = false;
console.error(err);
}
});
function goToLogin() {
mainRouter.push('/');
}
function goHome() {
mainRouter.push('/');
}
</script>
<style lang="scss" module>
.formContainer {
min-height: 100svh;
padding: 32px 32px 64px 32px;
box-sizing: border-box;
display: grid;
place-content: center;
position: relative;
}
.form {
position: relative;
z-index: 10;
border-radius: var(--MI-radius);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
overflow: clip;
max-width: 500px;
}
.banner {
padding: 16px;
text-align: center;
font-size: 26px;
background-color: var(--MI_THEME-accentedBg);
color: var(--MI_THEME-accent);
}
.title {
font-size: 1.2em;
font-weight: bold;
text-align: center;
margin-bottom: 16px;
}
.branding {
position: fixed;
bottom: 32px;
left: 32px;
color: #fff;
user-select: none;
pointer-events: none;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
.poweredBy {
margin-bottom: 2px;
font-size: 0.9em;
opacity: 0.8;
}
.misskey {
width: 120px;
@media (max-width: 450px) {
width: 100px;
}
}
</style>
+4 -6
View File
@@ -233,13 +233,11 @@ function addPluginHandler<K extends keyof HandlerDef>(installId: Plugin['install
}
export function launchPlugins() {
return Promise.all(prefer.s.plugins.map(plugin => {
for (const plugin of prefer.s.plugins) {
if (plugin.active) {
return launchPlugin(plugin.installId);
} else {
return Promise.resolve();
launchPlugin(plugin.installId);
}
}));
}
}
async function launchPlugin(id: Plugin['installId']): Promise<void> {
@@ -294,7 +292,7 @@ async function launchPlugin(id: Plugin['installId']): Promise<void> {
pluginContexts.set(plugin.installId, aiscript);
const parser = await getParser();
await aiscript.exec(parser.parse(plugin.src)).then(
aiscript.exec(parser.parse(plugin.src)).then(
() => {
console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
systemLog('Plugin started');
@@ -199,6 +199,9 @@ export const ROUTE_DEF = [{
}, {
path: '/reset-password/:token?',
component: page(() => import('@/pages/reset-password.vue')),
}, {
path: '/verify-email/:code',
component: page(() => import('@/pages/verify-email.vue')),
}, {
path: '/signup-complete/:code',
component: page(() => import('@/pages/signup-complete.vue')),
+1 -1
View File
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2025.8.0-beta.2",
"version": "2025.8.0-beta.1",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",