Merge branch 'pr/anatawa12/16484' into qr

This commit is contained in:
tamaina 2025-08-30 23:56:14 +09:00
commit e962c378e3
78 changed files with 385 additions and 261 deletions

View File

@ -6,8 +6,8 @@
### General
- ノートを削除した際、関連するノートが同時に削除されないようになりました
- APIで、「replyIdが存在しているのにreplyがnull」や「renoteIdが存在しているのにrenoteがnull」であるという、今までにはなかったパターンが表れることになります
- 定期的に参照されていない古いリモートの投稿を削除する機能が実装されました(コントロールパネル→パフォーマンス→Remote Notes Cleaning)
- 既存のサーバーでは**デフォルトでオフ**、新規サーバーでは**デフォルトでオン**になります
- 定期的に古いリモートの投稿を削除する機能が実装されました
- コントロールパネル→パフォーマンス→Remote Notes Cleaning で有効化できます
- データベースの肥大化を防止することが可能です
- 既存のサーバーで当機能を有効化した場合は、処理量が多くなるため、一時的にストレージ使用量が増加する可能性があります。
- 増加量を抑えるには、最大処理継続時間をデフォルトより短くしてください。
@ -15,7 +15,8 @@
- サーバーの初期設定が完了するまでは連合がオンにならないようになりました
- 日本語における公開範囲名称の「ダイレクト」が「指名」に改称されました
- 実際の動作に即した名称になり、馴染みのない人でも理解しやすくなりました
- 他サービスにおける「ダイレクトメッセージ」に相当するMisskeyの機能は「チャット」ですが、「ダイレクト投稿」という名称の機能が存在するとそちらがダイレクトメッセージ機能であるような誤解を生んでいました
- 他サービスにおける「ダイレクトメッセージ」に相当するMisskeyの機能は「チャット」ですが(過去のバージョンのMisskeyでも、当該機能は「チャット」ではなく「ダイレクトメッセージ」でした)、「ダイレクト投稿」という名称の機能が存在するとそちらがダイレクトメッセージ機能であるような誤解を生んでいました
- 今後、「チャット」の名称を「ダイレクトメッセージ」に戻す可能性があります
- mfm.jsをアップデートしました
- Enhance: Unicode 15.1 および 16.0 に収録されている絵文字に対応
- Enhance: acctに `.` が入っているユーザーのメンションに対応
@ -27,6 +28,7 @@
- プラグインは1.xに対応したものが必要です
- Playはそのまま動作しますが、新規に作られるプリセットは1.xになります
- 以前のバージョンから無効化されていた note_view_interruptor が有効になりました
- ハンドラは同期的である必要があります
- Feat: セーフモード
- プラグイン・テーマ・カスタムCSSの使用でクライアントの起動に問題が発生した際に、これらを無効にして起動できます
- 以下の方法でセーフモードを起動できます
@ -57,6 +59,7 @@
- Fix: 照会ダイアログでap/showでローカルユーザーを解決した際@username@nullに飛ばされる問題を修正
- Fix: アイコンのデコレーションを付ける際にデコレーションが表示されなくなる問題を修正
- Fix: 管理中アカウント一覧で正しい表示が行われない問題を修正
- Fix: lookupページでリモートURLを指定した際に正しく動作しない問題を修正
### Server
- Feat: サーバー管理コマンド
@ -71,6 +74,7 @@
- Fix: SystemWebhook設定でsecretを空に出来ない問題を修正
- Fix: 削除されたユーザーがチャットメッセージにリアクションしている場合`chat/history`などでエラーになる問題を修正
- Fix: Pageのアイキャッチ画像をドライブから消してもPageごと消えないように
- Fix: タイムラインAPIの withRenotes: false 時のレスポンスを修正
## 2025.7.0

View File

@ -1668,7 +1668,7 @@ _serverSettings:
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"
showActivitiesForVisitor: "Mostrar activitat"
_userGeneratedContentsVisibilityForVisitor:
all: "Tot obert al públic "
localOnly: "Només es publiquen els continguts locals, el contingut remot es manté privat"
@ -3120,6 +3120,7 @@ _serverSetupWizard:
youCanConfigureMoreFederationSettingsLater: "Les configuracions avançades, com especificar els servidors amb els quals es pot federar, es poden fer més tard."
remoteContentsCleaning: "Neteja automàtica del contingut rebut"
remoteContentsCleaning_description: "Quan es comença a federar es rep un munt de contingut, quan s'activa la neteja automàtica el contingut antic que no es consulta serà eliminat del servidor, el que permet estalviar espai d'emmagatzematge."
remoteContentsCleaning_description2: "Alguns mètodes de referència, com els enllaços, no poden ser detectats pel sistema."
adminInfo: "Informació de l'administrador "
adminInfo_description: "Estableix la informació de l'administrador que es farà servir per rebre consultes."
adminInfo_mustBeFilled: "Aquesta informació ha de ser omplerta si el servidor té els registres oberts o la federació es troba activada."

View File

@ -1218,8 +1218,8 @@ showRepliesToOthersInTimeline: "Show replies to others in timeline"
hideRepliesToOthersInTimeline: "Hide replies to others from timeline"
showRepliesToOthersInTimelineAll: "Show replies to others from everyone you follow in timeline"
hideRepliesToOthersInTimelineAll: "Hide replies to others from everyone you follow in timeline"
confirmShowRepliesAll: "This operation is irreversible. Would you really like to show replies to others from everyone you follow in your timeline?"
confirmHideRepliesAll: "This operation is irreversible. Would you really like to hide replies to others from everyone you follow in your timeline?"
confirmShowRepliesAll: "Are you sure you want to show replies from everyone you follow in your timeline? This action is irreversible."
confirmHideRepliesAll: "Are you sure you want to hide replies from everyone you follow in your timeline? This action is irreversible."
externalServices: "External Services"
sourceCode: "Source code"
sourceCodeIsNotYetProvided: "Source code is not yet available. Contact the administrator to fix this problem."
@ -1668,7 +1668,7 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "Some current settings will be reset."
entrancePageStyle: "Entrance page style"
showTimelineForVisitor: "Show timeline"
showActivityiesForVisitor: "Show activities"
showActivitiesForVisitor: "Show activities"
_userGeneratedContentsVisibilityForVisitor:
all: "Everything is public"
localOnly: "Only local content is published, remote content is kept private"
@ -3120,6 +3120,7 @@ _serverSetupWizard:
youCanConfigureMoreFederationSettingsLater: "Advanced settings such as specifying federated servers can be configured later."
remoteContentsCleaning: "Automatic cleanup of received contents"
remoteContentsCleaning_description: "Federation may result in a continuous inflow of content. Enabling automatic cleanup will remove outdated and unreferenced content from the server to save storage."
remoteContentsCleaning_description2: "Hyperlinks to remote content within the local server may become broken."
adminInfo: "Administrator information"
adminInfo_description: "Sets the administrator information used to receive inquiries."
adminInfo_mustBeFilled: "Must be entered if public server or federation is on."

View File

@ -1668,7 +1668,7 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "Algunas configuraciones actuales se restablecerán"
entrancePageStyle: "Estilo de la página de inicio"
showTimelineForVisitor: "Mostrar la línea de tiempo"
showActivityiesForVisitor: "Mostrar actividades"
showActivitiesForVisitor: "Mostrar actividades"
_userGeneratedContentsVisibilityForVisitor:
all: "Todo es público."
localOnly: "Sólo se publica el contenido local, el remoto se mantiene privado"
@ -3120,6 +3120,7 @@ _serverSetupWizard:
youCanConfigureMoreFederationSettingsLater: "Los ajustes avanzados, como la especificación de servidores federados, pueden configurarse más adelante."
remoteContentsCleaning: "Limpieza automática de los contenidos recibidos"
remoteContentsCleaning_description: "La federación puede dar lugar a un flujo continuo de contenido. Al habilitar la limpieza automática, se eliminará del servidor el contenido obsoleto y sin referencias para ahorrar espacio de almacenamiento."
remoteContentsCleaning_description2: "Ciertos métodos de referencia, como los hipervínculos, no pueden ser detectados por el sistema."
adminInfo: "Información del administrador"
adminInfo_description: "Establece la información del administrador para recibir consultas."
adminInfo_mustBeFilled: "Esta información debe ser introducida en el caso de registros abiertos o la federación esté activada."

10
locales/index.d.ts vendored
View File

@ -6531,7 +6531,7 @@ export interface Locale extends ILocale {
*/
"remoteNotesCleaning": string;
/**
* 稿
* 稿
*/
"remoteNotesCleaning_description": string;
/**
@ -12032,17 +12032,13 @@ export interface Locale extends ILocale {
*/
"youCanConfigureMoreFederationSettingsLater": string;
/**
*
*
*/
"remoteContentsCleaning": string;
/**
*
*
*/
"remoteContentsCleaning_description": string;
/**
*
*/
"remoteContentsCleaning_description2": string;
/**
*
*/

View File

@ -1668,7 +1668,7 @@ _serverSettings:
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"
showActivitiesForVisitor: "Mostrare la propria attività"
_userGeneratedContentsVisibilityForVisitor:
all: "Tutto pubblico"
localOnly: "Pubblica solo contenuti locali, mantieni privati i contenuti remoti"
@ -3120,6 +3120,7 @@ _serverSetupWizard:
youCanConfigureMoreFederationSettingsLater: "Puoi svolgere la configurazione avanzata anche dopo. Ad esempio specificando quali server possono federarsi."
remoteContentsCleaning: "Pulizia automatica dei contenuti in arrivo"
remoteContentsCleaning_description: "Con la federazione funzionante, riceverai sempre più contenuti. Abilitando la pulizia automatica, i contenuti non referenziati e obsoleti verranno rimossi automaticamente dai tuoi server, risparmiando spazio di archiviazione."
remoteContentsCleaning_description2: "Alcuni metodi di riferimento, come i collegamenti ipertestuali, non possono essere rilevati sul sistema."
adminInfo: "Informazioni sull'amministratore"
adminInfo_description: "Imposta le informazioni dell'amministratore utilizzate per accettare le richieste."
adminInfo_mustBeFilled: "Questa operazione è necessaria su un server aperto o se è attiva la federazione."

View File

@ -1660,7 +1660,7 @@ _serverSettings:
fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。"
reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。"
remoteNotesCleaning: "リモート投稿の自動クリーニング"
remoteNotesCleaning_description: "有効にすると、参照されていない古いリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。"
remoteNotesCleaning_description: "有効にすると、一定期間経過したリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。"
remoteNotesCleaningMaxProcessingDuration: "最大クリーニング処理継続時間"
remoteNotesCleaningExpiryDaysForEachNotes: "最低ノート保持日数"
inquiryUrl: "問い合わせ先URL"
@ -3216,9 +3216,8 @@ _serverSetupWizard:
doYouConnectToFediverse_description1: "分散型サーバーで構成されるネットワーク(Fediverse)に接続すると、他のサーバーと相互にコンテンツのやり取りが可能です。"
doYouConnectToFediverse_description2: "Fediverseと接続することは「連合」とも呼ばれます。"
youCanConfigureMoreFederationSettingsLater: "連合可能なサーバーの指定など、高度な設定も後ほど可能です。"
remoteContentsCleaning: "受信コンテンツの自動クリーニング"
remoteContentsCleaning_description: "連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったコンテンツを自動でサーバーから削除し、ストレージを節約できます。"
remoteContentsCleaning_description2: "ハイパーリンクなど、一部の参照方法はシステム上で検知できません。"
remoteContentsCleaning: "リモートコンテンツの自動クリーニング"
remoteContentsCleaning_description: "連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、一定期間経過したリモートコンテンツを自動でサーバーから削除し、ストレージを節約できます。"
adminInfo: "管理者情報"
adminInfo_description: "問い合わせを受け付けるために使用される管理者情報を設定します。"
adminInfo_mustBeFilled: "オープンサーバー、または連合がオンの場合は必ず入力が必要です。"

View File

@ -1668,7 +1668,7 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "현재 일부 설정은 리셋됩니다."
entrancePageStyle: "입구 페이지의 스타일"
showTimelineForVisitor: "타임라인 표시"
showActivityiesForVisitor: "활동 표시"
showActivitiesForVisitor: "액티비티 표시하기"
_userGeneratedContentsVisibilityForVisitor:
all: "모두 공개"
localOnly: "로컬 콘텐츠만 공개하고 리모트 콘텐츠는 비공개"
@ -3120,6 +3120,7 @@ _serverSetupWizard:
youCanConfigureMoreFederationSettingsLater: "나중에 연합 가능한 서버의 지정 등 고급 설정을 할 수 있습니다."
remoteContentsCleaning: "리모트 콘텐츠 자동 정리"
remoteContentsCleaning_description: "연합 중인 서버가 있는 경우, 리모트 서버에서 대단히 많은 콘텐츠를 받아오게 됩니다. 자동 정리 기능을 활성화하면, 오래되고 서버에서 더 이상 조회되지 않는 콘텐츠를 자동으로 서버에서 삭제하여, 스토리지를 절약할 수 있습니다."
remoteContentsCleaning_description2: "로컬 내 원격 콘텐츠로의 하이퍼링크는 깨진 링크로 됩니다."
adminInfo: "관리자 정보"
adminInfo_description: "문의 접수를 위해 사용되는 관리자 정보를 설정합니다."
adminInfo_mustBeFilled: "오픈 서버 혹은 연합이 켜져 있는 경우 반드시 입력해야 합니다."

View File

@ -1668,7 +1668,6 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "Bazı mevcut ayarlar sıfırlanacaktır."
entrancePageStyle: "Giriş sayfası stili"
showTimelineForVisitor: "Panoyu göster"
showActivityiesForVisitor: "Aktiviteleri göster"
_userGeneratedContentsVisibilityForVisitor:
all: "Her şey halka açıktır."
localOnly: "Yalnızca yerel içerik yayınlanır, uzak içerik gizli tutulur."

View File

@ -1668,7 +1668,7 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "现有的部分设定将重置。"
entrancePageStyle: "入口页面样式"
showTimelineForVisitor: "显示时间线"
showActivityiesForVisitor: "显示活动"
showActivitiesForVisitor: "显示活动"
_userGeneratedContentsVisibilityForVisitor:
all: "全部公开"
localOnly: "仅公开本地内容,隐藏远程内容"
@ -3120,6 +3120,7 @@ _serverSetupWizard:
youCanConfigureMoreFederationSettingsLater: "可在之后进行如哪些服务器可以进行联合等高级设置。"
remoteContentsCleaning: "自动清理传入内容"
remoteContentsCleaning_description: "加入联合后,服务器将持续接收大量内容。打开自动清理后,将自动删除无法找到的旧内容,可节省存储空间。"
remoteContentsCleaning_description2: "如超链接之类的某些引用方法无法被系统检测到。"
adminInfo: "管理员信息"
adminInfo_description: "设置用于接受询问的管理员信息。"
adminInfo_mustBeFilled: "开放服务器或开启了联合的情况下必须输入。"

View File

@ -1668,7 +1668,7 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "當前的部分設定將會被重設。"
entrancePageStyle: "入口頁面的樣式"
showTimelineForVisitor: "顯示時間軸"
showActivityiesForVisitor: "顯示活動"
showActivitiesForVisitor: "顯示活動"
_userGeneratedContentsVisibilityForVisitor:
all: "全部公開\n"
localOnly: "僅公開本地內容,遠端內容則不公開\n"
@ -3120,6 +3120,7 @@ _serverSetupWizard:
youCanConfigureMoreFederationSettingsLater: "您可以在稍後進行更高級的設定,例如指定可以聯繫的伺服器等。\n"
remoteContentsCleaning: "自動清理接收的內容"
remoteContentsCleaning_description: "進行聯邦後,會持續接收大量內容。啟用自動清理功能後,系統會自動從伺服器中刪除未被參照的過時內容,以節省儲存空間。"
remoteContentsCleaning_description2: "有些引用方式系統上無法檢測到,例如超連結。"
adminInfo: "管理員資訊"
adminInfo_description: "設定用於接收查詢的管理者資訊。\n"
adminInfo_mustBeFilled: "當設置為開放伺服器或啟用聯邦時,必須填寫此資訊。\n"

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2025.8.0-beta.4",
"version": "2025.8.0-beta.6",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@ -244,7 +244,6 @@ export class WebhookTestService {
case 'reaction':
return;
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _exhaustiveAssertion: never = params.type;
return;
}
@ -327,7 +326,6 @@ export class WebhookTestService {
break;
}
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _exhaustiveAssertion: never = params.type;
return;
}
@ -412,7 +410,7 @@ export class WebhookTestService {
name: user.name,
username: user.username,
host: user.host,
avatarUrl: user.avatarId == null ? null : user.avatarUrl,
avatarUrl: (user.avatarId == null ? null : user.avatarUrl) ?? '',
avatarBlurhash: user.avatarId == null ? null : user.avatarBlurhash,
avatarDecorations: user.avatarDecorations.map(it => ({
id: it.id,

View File

@ -471,8 +471,8 @@ export class UserEntityService implements OnModuleInit {
(profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
null;
const isModerator = isMe && isDetailed ? this.roleService.isModerator(user) : null;
const isAdmin = isMe && isDetailed ? this.roleService.isAdministrator(user) : null;
const isModerator = isMe && isDetailed ? this.roleService.isModerator(user) : undefined;
const isAdmin = isMe && isDetailed ? this.roleService.isAdministrator(user) : undefined;
const unreadAnnouncements = isMe && isDetailed ?
(await this.announcementService.getUnreadAnnouncements(user)).map((announcement) => ({
createdAt: this.idService.parse(announcement.id).date.toISOString(),
@ -481,6 +481,7 @@ export class UserEntityService implements OnModuleInit {
const notificationsInfo = isMe && isDetailed ? await this.getNotificationsInfo(user.id) : null;
// TODO: 例えば avatarUrl: true など間違った型を設定しても型エラーにならないのをどうにかする(ジェネリクス使わない方法で実装するしかなさそう?)
const packed = {
id: user.id,
name: user.name,

View File

@ -65,6 +65,7 @@ import {
packedMetaDetailedSchema,
packedMetaLiteSchema,
} from '@/models/json-schema/meta.js';
import { packedUserWebhookSchema } from '@/models/json-schema/user-webhook.js';
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js';
import { packedChatMessageSchema, packedChatMessageLiteSchema, packedChatMessageLiteForRoomSchema, packedChatMessageLiteFor1on1Schema } from '@/models/json-schema/chat-message.js';
@ -134,6 +135,7 @@ export const refs = {
MetaLite: packedMetaLiteSchema,
MetaDetailedOnly: packedMetaDetailedOnlySchema,
MetaDetailed: packedMetaDetailedSchema,
UserWebhook: packedUserWebhookSchema,
SystemWebhook: packedSystemWebhookSchema,
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
ChatMessage: packedChatMessageSchema,

View File

@ -0,0 +1,55 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { webhookEventTypes } from '@/models/Webhook.js';
export const packedUserWebhookSchema = {
type: 'object',
properties: {
id: {
type: 'string',
format: 'id',
optional: false, nullable: false,
},
userId: {
type: 'string',
format: 'id',
optional: false, nullable: false,
},
name: {
type: 'string',
optional: false, nullable: false,
},
on: {
type: 'array',
items: {
type: 'string',
optional: false, nullable: false,
enum: webhookEventTypes,
},
},
url: {
type: 'string',
optional: false, nullable: false,
},
secret: {
type: 'string',
optional: false, nullable: false,
},
active: {
type: 'boolean',
optional: false, nullable: false,
},
latestSentAt: {
type: 'string',
format: 'date-time',
optional: false, nullable: true,
},
latestStatus: {
type: 'integer',
optional: false, nullable: true,
},
},
} as const;

View File

@ -65,7 +65,7 @@ export const packedUserLiteSchema = {
avatarUrl: {
type: 'string',
format: 'url',
nullable: true, optional: false,
nullable: false, optional: false,
},
avatarBlurhash: {
type: 'string',
@ -465,11 +465,11 @@ export const packedMeDetailedOnlySchema = {
},
isModerator: {
type: 'boolean',
nullable: true, optional: false,
nullable: false, optional: false,
},
isAdmin: {
type: 'boolean',
nullable: true, optional: false,
nullable: false, optional: false,
},
injectFeaturedNote: {
type: 'boolean',
@ -591,7 +591,7 @@ export const packedMeDetailedOnlySchema = {
},
mutedInstances: {
type: 'array',
nullable: true, optional: false,
nullable: false, optional: false,
items: {
type: 'string',
nullable: false, optional: false,

View File

@ -157,6 +157,22 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
maybeSensitive: {
type: 'boolean',
optional: false, nullable: false,
},
maybePorn: {
type: 'boolean',
optional: false, nullable: false,
},
requestIp: {
type: 'string',
optional: false, nullable: true,
},
requestHeaders: {
type: 'object',
optional: false, nullable: true,
},
},
},
} as const;

View File

@ -223,10 +223,12 @@ export const meta = {
sensitiveMediaDetection: {
type: 'string',
optional: false, nullable: false,
enum: ['none', 'all', 'local', 'remote'],
},
sensitiveMediaDetectionSensitivity: {
type: 'string',
optional: false, nullable: false,
enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'],
},
setSensitiveFlagAutomatically: {
type: 'boolean',
@ -473,6 +475,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
feedbackUrl: {
type: 'string',
optional: false, nullable: true,
},
summalyProxy: {
type: 'string',
optional: false, nullable: true,

View File

@ -18,9 +18,14 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import type { Config } from '@/config.js';
import { ApiError } from '../../error.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
import * as Acct from '@/misc/acct.js';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { NotesRepository, UsersRepository } from '@/models/_.js';
export const meta = {
tags: ['federation'],
@ -109,6 +114,12 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private utilityService: UtilityService,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
@ -137,7 +148,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
let local = await this.mergePack(me, ...await Promise.all([
this.apDbResolverService.getUserFromApId(uri),
this.apDbResolverService.getUserFromApId(uri).then(async x => x ?? await this.getUserFromProfileUrl(uri)),
this.apDbResolverService.getNoteFromApId(uri),
]));
if (local != null) return local;
@ -200,6 +211,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
);
}
// To keep consistency with ap/show from external server,
// this function will handle `https://local.instance/@user`
// profile url to resolve user.
@bindThis
private async getUserFromProfileUrl(url: string): Promise<MiUser | null | undefined> {
if (!this.utilityService.isUriLocal(url)) {
return null;
}
const uri = new URL(url);
const pathComponents = uri.pathname.split('/').filter(Boolean);
if (pathComponents.length === 1 && pathComponents[0].startsWith('@')) {
const acct = Acct.parse(pathComponents[0]);
// normalize acct host
if (acct.host != null && this.utilityService.toPuny(acct.host) === this.utilityService.toPuny(this.config.host)) acct.host = null;
return await this.usersRepository.findOneBy({
usernameLower: acct.username.toLowerCase(),
host: acct.host ?? IsNull(),
isSuspended: false,
});
}
return null;
}
@bindThis
private async mergePack(me: MiLocalUser | null | undefined, user: MiUser | null | undefined, note: MiNote | null | undefined): Promise<SchemaType<typeof meta.res> | null> {
if (user != null) {

View File

@ -46,6 +46,14 @@ export const meta = {
type: 'string',
},
},
iconUrl: {
type: 'string',
optional: true, nullable: true,
},
description: {
type: 'string',
optional: true, nullable: true,
},
},
},
},
@ -88,6 +96,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
createdAt: this.idService.parse(token.id).date.toISOString(),
lastUsedAt: token.lastUsedAt?.toISOString(),
permission: token.app ? token.app.permission : token.permission,
iconUrl: token.iconUrl,
description: token.description ?? token.app?.description ?? null,
})));
});
}

View File

@ -21,29 +21,7 @@ export const meta = {
type: 'array',
items: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
userId: {
type: 'string',
format: 'misskey:id',
},
name: { type: 'string' },
on: {
type: 'array',
items: {
type: 'string',
enum: webhookEventTypes,
},
},
url: { type: 'string' },
secret: { type: 'string' },
active: { type: 'boolean' },
latestSentAt: { type: 'string', format: 'date-time', nullable: true },
latestStatus: { type: 'integer', nullable: true },
},
ref: 'UserWebhook',
},
},
} as const;
@ -65,19 +43,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: me.id,
});
return webhooks.map(webhook => (
{
id: webhook.id,
userId: webhook.userId,
name: webhook.name,
on: webhook.on,
url: webhook.url,
secret: webhook.secret,
active: webhook.active,
latestSentAt: webhook.latestSentAt ? webhook.latestSentAt.toISOString() : null,
latestStatus: webhook.latestStatus,
}
));
return webhooks.map(webhook => ({
id: webhook.id,
userId: webhook.userId,
name: webhook.name,
on: webhook.on,
url: webhook.url,
secret: webhook.secret,
active: webhook.active,
latestSentAt: webhook.latestSentAt ? webhook.latestSentAt.toISOString() : null,
latestStatus: webhook.latestStatus,
}));
});
}
}

View File

@ -28,29 +28,7 @@ export const meta = {
res: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
userId: {
type: 'string',
format: 'misskey:id',
},
name: { type: 'string' },
on: {
type: 'array',
items: {
type: 'string',
enum: webhookEventTypes,
},
},
url: { type: 'string' },
secret: { type: 'string' },
active: { type: 'boolean' },
latestSentAt: { type: 'string', format: 'date-time', nullable: true },
latestStatus: { type: 'integer', nullable: true },
},
ref: 'UserWebhook',
},
} as const;

View File

@ -91,6 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
qb.orWhere(new Brackets(qb => {
qb.where('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}));
}

View File

@ -242,6 +242,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}));
}

View File

@ -223,6 +223,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}));
}

View File

@ -8,7 +8,7 @@
import { parse as vueSfcParse } from 'vue/compiler-sfc';
import {
createLogger,
EnvironmentModuleGraph,
type EnvironmentModuleGraph,
type LogErrorOptions,
type LogOptions,
normalizePath,

View File

@ -4,11 +4,11 @@
*/
import { utils, values } from '@syuilo/aiscript';
import { genId } from '@/utility/id.js';
import { ref } from 'vue';
import type { Ref } from 'vue';
import * as Misskey from 'misskey-js';
import { assertStringAndIsIn } from './common.js';
import type { Ref } from 'vue';
import { genId } from '@/utility/id.js';
const ALIGNS = ['left', 'center', 'right'] as const;
const FONTS = ['serif', 'sans-serif', 'monospace'] as const;
@ -21,16 +21,15 @@ type BorderStyle = (typeof BORDER_STYLES)[number];
export type AsUiComponentBase = {
id: string;
hidden?: boolean;
children?: AsUiComponent['id'][];
};
export type AsUiRoot = AsUiComponentBase & {
type: 'root';
children: AsUiComponent['id'][];
};
export type AsUiContainer = AsUiComponentBase & {
type: 'container';
children?: AsUiComponent['id'][];
align?: Align;
bgColor?: string;
fgColor?: string;
@ -123,7 +122,6 @@ export type AsUiSelect = AsUiComponentBase & {
export type AsUiFolder = AsUiComponentBase & {
type: 'folder';
children?: AsUiComponent['id'][];
title?: string;
opened?: boolean;
};

View File

@ -61,8 +61,8 @@ import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/utili
const props = withDefaults(defineProps<{
user: Misskey.entities.User;
withLocked: boolean;
withDescription: boolean;
withLocked?: boolean;
withDescription?: boolean;
}>(), {
withLocked: true,
withDescription: true,

View File

@ -3,14 +3,13 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable import/no-default-export */
import type { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw';
import { action } from 'storybook/actions';
import { channel } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkChannelList from './MkChannelList.vue';
import type { StoryObj } from '@storybook/vue3';
import { Paginator } from '@/utility/paginator.js';
export const Default = {
render(args) {
return {
@ -33,10 +32,7 @@ export const Default = {
};
},
args: {
pagination: {
endpoint: 'channels/search',
limit: 10,
},
paginator: new Paginator('channels/search', {}),
},
parameters: {
chromatic: {

View File

@ -152,11 +152,12 @@ import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
import { Paginator } from '@/utility/paginator.js';
const props = withDefaults(defineProps<{
initialFolder?: Misskey.entities.DriveFolder['id'] | null;
initialFolder?: Misskey.entities.DriveFolder | Misskey.entities.DriveFolder['id'] | null;
type?: string;
multiple?: boolean;
select?: 'file' | 'folder' | null;
}>(), {
initialFolder: null,
multiple: false,
select: null,
});

View File

@ -39,13 +39,13 @@ withDefaults(defineProps<{
});
const emit = defineEmits<{
(ev: 'done', r?: Misskey.entities.DriveFolder[]): void;
(ev: 'done', r?: (Misskey.entities.DriveFolder | null)[]): void;
(ev: 'closed'): void;
}>();
const dialog = useTemplateRef('dialog');
const selected = ref<Misskey.entities.DriveFolder[]>([]);
const selected = ref<(Misskey.entities.DriveFolder | null)[]>([]);
function ok() {
emit('done', selected.value);
@ -57,7 +57,7 @@ function cancel() {
dialog.value?.close();
}
function onChangeSelection(v: Misskey.entities.DriveFolder[]) {
function onChangeSelection(v: (Misskey.entities.DriveFolder | null)[]) {
selected.value = v;
}
</script>

View File

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm class="summaryMfm" :text="flash.summary" :plain="true" :nowrap="true"/>
</p>
<footer>
<img v-if="flash.user.avatarUrl != null" class="icon" :src="flash.user.avatarUrl"/>
<img class="icon" :src="flash.user.avatarUrl"/>
<p>{{ userName(flash.user) }}</p>
</footer>
</article>

View File

@ -34,9 +34,10 @@ import { deviceKind } from '@/utility/device-kind.js';
import { prefer } from '@/preferences.js';
const props = withDefaults(defineProps<{
anchorElement?: HTMLElement;
anchorElement?: HTMLElement | null;
anchor?: { x: string; y: string; };
}>(), {
anchorElement: null,
anchor: () => ({ x: 'right', y: 'center' }),
});

View File

@ -94,6 +94,8 @@ async function calcAspectRatio() {
onMounted(() => {
calcAspectRatio();
if (gallery.value == null) return; // TS
lightbox = new PhotoSwipeLightbox({
dataSource: props.mediaList
.filter(media => {

View File

@ -23,8 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<div :class="$style.root" class="_forceShrinkSpacer">
<StackingRouterView v-if="prefer.s['experimental.stackingRouterView']" :key="reloadCount" :router="windowRouter"/>
<RouterView v-else :key="reloadCount" :router="windowRouter"/>
<StackingRouterView v-if="prefer.s['experimental.stackingRouterView']" :key="reloadCount.toString() + ':stacking'" :router="windowRouter"/>
<RouterView v-else :key="reloadCount.toString() + ':non-stacking'" :router="windowRouter"/>
</div>
</MkWindow>
</template>
@ -58,20 +58,15 @@ const windowRouter = createRouter(props.initialPath);
const pageMetadata = ref<null | PageMetadata>(null);
const windowEl = useTemplateRef('windowEl');
const history = ref<{ path: string; }[]>([{
const _history_ = ref<{ path: string; }[]>([{
path: windowRouter.getCurrentFullPath(),
}]);
const buttonsLeft = computed(() => {
const buttons: Record<string, unknown>[] = [];
if (history.value.length > 1) {
buttons.push({
icon: 'ti ti-arrow-left',
onClick: back,
});
}
return buttons;
return _history_.value.length > 1 ? [{
icon: 'ti ti-arrow-left',
title: i18n.ts.goBack,
onClick: back,
}] : [];
});
const buttonsRight = computed(() => {
const buttons = [{
@ -97,12 +92,12 @@ function getSearchMarker(path: string) {
const searchMarkerId = ref<string | null>(getSearchMarker(props.initialPath));
windowRouter.addListener('push', ctx => {
history.value.push({ path: ctx.fullPath });
_history_.value.push({ path: ctx.fullPath });
});
windowRouter.addListener('replace', ctx => {
history.value.pop();
history.value.push({ path: ctx.fullPath });
_history_.value.pop();
_history_.value.push({ path: ctx.fullPath });
});
windowRouter.addListener('change', ctx => {
@ -150,8 +145,8 @@ const contextmenu = computed(() => ([{
}]));
function back() {
history.value.pop();
windowRouter.replaceByPath(history.value.at(-1)!.path);
_history_.value.pop();
windowRouter.replaceByPath(_history_.value.at(-1)!.path);
}
function reload() {

View File

@ -78,7 +78,7 @@ function subscribe() {
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
return promiseDialog(registration.value.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey),
applicationServerKey: urlBase64ToBase64(instance.swPublickey),
})
.then(async subscription => {
pushSubscription.value = subscription;
@ -131,22 +131,16 @@ function encode(buffer: ArrayBuffer | null) {
}
/**
* Convert the URL safe base64 string to a Uint8Array
* Convert the URL safe base64 string to a base64 string
* @param base64String base64 string
*/
function urlBase64ToUint8Array(base64String: string): Uint8Array {
function urlBase64ToBase64(base64String: string): string {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
return base64;
}
if (navigator.serviceWorker == null) {

View File

@ -105,9 +105,7 @@ async function addRole() {
.map(r => ({ text: r.name, value: r }));
const { canceled, result: role } = await os.select({ items });
if (canceled) {
return;
}
if (canceled || role == null) return;
selectedRoleIds.value.push(role.id);
}

View File

@ -66,7 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-if="q_federation === 'yes'" v-model="q_remoteContentsCleaning">
<template #label>{{ i18n.ts._serverSetupWizard.remoteContentsCleaning }}</template>
<template #caption>{{ i18n.ts._serverSetupWizard.remoteContentsCleaning_description }} ({{ i18n.ts._serverSetupWizard.remoteContentsCleaning_description2 }})</template>
<template #caption>{{ i18n.ts._serverSetupWizard.remoteContentsCleaning_description }}</template>
</MkSwitch>
</div>
</MkFolder>

View File

@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" provider="testcaptcha"/>
<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" provider="testcaptcha" :sitekey="null"/>
</div>
<MkButton type="submit" :disabled="needCaptcha && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>

View File

@ -59,7 +59,7 @@ import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-sep
import { Paginator } from '@/utility/paginator.js';
const props = defineProps<{
excludeTypes?: typeof notificationTypes[number][];
excludeTypes?: typeof notificationTypes[number][] | null;
}>();
const rootEl = useTemplateRef('rootEl');

View File

@ -51,6 +51,7 @@ export type DefaultStoredWidget = {
<script lang="ts" setup>
import { defineAsyncComponent, ref, computed } from 'vue';
import { isLink } from '@@/js/is-link.js';
import { genId } from '@/utility/id.js';
import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue';
@ -58,7 +59,6 @@ import { widgets as widgetDefs, federationWidgets } from '@/widgets/index.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { isLink } from '@@/js/is-link.js';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@ -81,7 +81,7 @@ const emit = defineEmits<{
(ev: 'updateWidgets', widgets: Widget[]): void;
(ev: 'addWidget', widget: Widget): void;
(ev: 'removeWidget', widget: Widget): void;
(ev: 'updateWidget', widget: Partial<Widget>): void;
(ev: 'updateWidget', widget: { id: Widget['id']; data: Widget['data']; }): void;
(ev: 'exit'): void;
}>();
@ -104,7 +104,7 @@ const addWidget = () => {
const removeWidget = (widget) => {
emit('removeWidget', widget);
};
const updateWidget = (id, data) => {
const updateWidget = (id: Widget['id'], data: Widget['data']) => {
emit('updateWidget', { id, data });
};

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkLoading/>
</div>
<div v-else-if="resolved">
<slot :result="result"></slot>
<slot :result="result as T"></slot>
</div>
<div v-else>
<div :class="$style.error">

View File

@ -2,11 +2,10 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { expect, userEvent, waitFor, within } from '@storybook/test';
import type { StoryObj } from '@storybook/vue3';
import MkAd from './MkAd.vue';
import type { StoryObj } from '@storybook/vue3';
import { i18n } from '@/i18n.js';
const common = {
@ -68,7 +67,7 @@ const common = {
await expect(imgAgain).toBeInTheDocument();
},
args: {
prefer: [],
preferForms: [],
specify: {
id: 'someadid',
ratio: 1,

View File

@ -84,7 +84,6 @@ const bound = computed(() => props.link
: {});
const url = computed(() => {
if (props.user.avatarUrl == null) return null;
if (prefer.s.disableShowingAnimatedImages || prefer.s.dataSaver.avatar) return getStaticImageUrl(props.user.avatarUrl);
return props.user.avatarUrl;
});

View File

@ -2,7 +2,7 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { waitFor } from '@storybook/test';
import MkPageHeader from './MkPageHeader.vue';
import type { StoryObj } from '@storybook/vue3';
@ -59,6 +59,7 @@ export const Icon = {
{
...OneTab.args.tabs[0],
icon: 'ti ti-home',
title: 'Home',
},
],
},
@ -71,6 +72,7 @@ export const IconOnly = {
{
key: Icon.args.tabs[0].key,
icon: Icon.args.tabs[0].icon,
title: Icon.args.tabs[0].title,
iconOnly: true,
},
],

View File

@ -30,19 +30,21 @@ const props = defineProps<{
router?: Router;
}>();
const router = props.router ?? inject(DI.router);
const _router = props.router ?? inject(DI.router);
if (router == null) {
if (_router == null) {
throw new Error('no router provided');
}
const router = _router;
const viewId = randomId();
provide(DI.viewId, viewId);
const currentDepth = inject(DI.routerCurrentDepth, 0);
provide(DI.routerCurrentDepth, currentDepth + 1);
const current = router.current!;
const current = router.current;
const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage);
const currentPageProps = ref(current.props);
let currentRoutePath = current.route.path;
@ -52,14 +54,10 @@ router.useListener('change', ({ resolved }) => {
if (resolved == null || 'redirect' in resolved.route) return;
if (resolved.route.path === currentRoutePath && deepEqual(resolved.props, currentPageProps.value)) return;
function _() {
currentPageComponent.value = resolved.route.component;
currentPageProps.value = resolved.props;
key.value = router.getCurrentFullPath();
currentRoutePath = resolved.route.path;
}
_();
currentPageComponent.value = resolved.route.component;
currentPageProps.value = resolved.props;
key.value = router.getCurrentFullPath();
currentRoutePath = resolved.route.path;
});
</script>

View File

@ -87,7 +87,7 @@ router.useListener('change', ({ resolved }) => {
const fullPath = router.getCurrentFullPath();
if (tabs.value.some(tab => tab.routePath === routePath && deepEqual(resolved.props, tab.props))) {
const newTabs = [];
const newTabs = [] as typeof tabs.value;
for (const tab of tabs.value) {
newTabs.push(tab);

View File

@ -48,7 +48,7 @@ import { $i } from '@/i.js';
const customEmojiTags = getCustomEmojiTags();
const q = ref('');
const searchEmojis = ref<Misskey.entities.EmojiSimple[]>(null);
const searchEmojis = ref<Misskey.entities.EmojiSimple[] | null>(null);
const selectedTags = ref(new Set());
function search() {

View File

@ -16,9 +16,11 @@ import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue';
import MkAchievements from '@/components/MkAchievements.vue';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { $i } from '@/i.js';
import { ensureSignin } from '@/i.js';
import { claimAchievement } from '@/utility/achievements.js';
const $i = ensureSignin();
let timer: number | null;
function viewAchievements3min() {

View File

@ -78,7 +78,7 @@ const timeline = computed(() => {
return paginator.items.value.map(x => ({
id: x.id,
timestamp: new Date(x.createdAt).getTime(),
data: x,
data: x as Misskey.entities.ModerationLog,
}));
});

View File

@ -68,6 +68,11 @@ function send() {
function onEndpointChange() {
misskeyApi('endpoint', { endpoint: endpoint.value }, withCredential.value ? undefined : null).then(resp => {
if (resp == null) {
body.value = '{}';
return;
}
const endpointBody = {};
for (const p of resp.params) {
endpointBody[p.name] =

View File

@ -110,6 +110,11 @@ async function search() {
const type = searchType.value.toString().trim();
if (type !== 'nameAndDescription' && type !== 'nameOnly') {
console.error(`Unrecognized search type: ${type}`);
return;
}
channelPaginator.value = markRaw(new Paginator('channels/search', {
limit: 10,
params: {

View File

@ -156,12 +156,9 @@ async function done() {
isSensitive: isSensitive.value,
localOnly: localOnly.value,
roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id),
fileId: file.value ? file.value.id : undefined,
};
if (file.value) {
params.fileId = file.value.id;
}
if (props.emoji) {
await os.apiWithDialog('admin/emoji/update', {
id: props.emoji.id,
@ -177,7 +174,12 @@ async function done() {
windowEl.value?.close();
} else {
const created = await os.apiWithDialog('admin/emoji/add', params);
if (params.fileId == null) return;
const created = await os.apiWithDialog('admin/emoji/add', {
...params,
fileId: params.fileId, // TS
});
emit('done', {
created: created,

View File

@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</div>
<div v-else>
<MkFoldableSection ref="tagsEl" :foldable="true" :expanded="false" class="_margin">
<MkFoldableSection :foldable="true" :expanded="false" class="_margin">
<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template>
<div>
@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFoldableSection>
<MkFoldableSection v-if="tag != null" :key="`${tag}`" class="_margin">
<MkFoldableSection v-if="tagUsersPaginator != null" :key="`${tag}`" class="_margin">
<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
<MkUserList :paginator="tagUsersPaginator"/>
</MkFoldableSection>
@ -78,22 +78,17 @@ const props = defineProps<{
}>();
const origin = ref('local');
const tagsEl = useTemplateRef('tagsEl');
const tagsLocal = ref<Misskey.entities.Hashtag[]>([]);
const tagsRemote = ref<Misskey.entities.Hashtag[]>([]);
watch(() => props.tag, () => {
if (tagsEl.value) tagsEl.value.toggleContent(props.tag == null);
});
const tagUsersPaginator = markRaw(new Paginator('hashtags/users', {
const tagUsersPaginator = props.tag != null ? markRaw(new Paginator('hashtags/users', {
limit: 30,
params: {
tag: props.tag,
origin: 'combined',
sort: '+follower',
},
}));
})) : null;
const pinnedUsersPaginator = markRaw(new Paginator('pinned-users', {
noPaging: true,

View File

@ -196,13 +196,13 @@ async function run() {
const isLegacy = !flash.value.script.replaceAll(' ', '').startsWith('///@1.0.0');
const { Interpreter, Parser, values } = isLegacy ? await import('@syuilo/aiscript-0-19-0') : await import('@syuilo/aiscript');
const { Interpreter, Parser, values } = isLegacy ? (await import('@syuilo/aiscript-0-19-0') as any) : await import('@syuilo/aiscript');
const parser = new Parser();
components.value = [];
aiscript.value = new Interpreter({
const interpreter = new Interpreter({
...createAiScriptEnv({
storageKey: 'flash:' + flash.value.id,
}),
@ -221,6 +221,8 @@ async function run() {
},
});
aiscript.value = interpreter;
let ast;
try {
ast = parser.parse(flash.value.script);
@ -232,8 +234,8 @@ async function run() {
return;
}
try {
await aiscript.value.exec(ast);
} catch (err) {
await interpreter.exec(ast);
} catch (err: any) {
os.alert({
type: 'error',
title: 'AiScript Error',

View File

@ -48,7 +48,7 @@ function _fetch_() {
if (res.type === 'User') {
mainRouter.replace('/@:acct/:page?', {
params: {
acct: res.host != null ? `${res.object.username}@${res.object.host}` : res.object.username,
acct: res.object.host != null ? `${res.object.username}@${res.object.host}` : res.object.username,
},
});
} else if (res.type === 'Note') {

View File

@ -82,10 +82,10 @@ const logs = ref<{
text: string;
print: boolean;
}[]>([]);
const root = ref<AsUiRoot>();
const root = ref<AsUiRoot | undefined>();
const components = ref<Ref<AsUiComponent>[]>([]);
const uiKey = ref(0);
const uiInspectorOpenedComponents = ref(new Map<string, boolean>);
const uiInspectorOpenedComponents = ref(new WeakMap<AsUiComponent | Ref<AsUiComponent>, boolean>);
const saved = miLocalStorage.getItem('scratchpad');
if (saved) {
@ -186,11 +186,13 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
const showns = computed(() => {
if (root.value == null) return new Set<string>();
const result = new Set<string>();
(function addChildrenToResult(c: AsUiComponent) {
result.add(c.id);
if (c.children) {
const childComponents = components.value.filter(v => c.children.includes(v.value.id));
const children = c.children;
if (children) {
const childComponents = components.value.filter(v => children.includes(v.value.id));
for (const child of childComponents) {
addChildrenToResult(child.value);
}

View File

@ -91,7 +91,7 @@ async function addItem() {
value: '-', text: i18n.ts.divider,
}],
});
if (canceled) return;
if (canceled || item == null) return;
items.value = [...items.value, {
id: genId(),
type: item,

View File

@ -55,11 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPushNotificationAllowButton ref="allowButton"/>
<MkSwitch :disabled="!pushRegistrationInServer" :modelValue="sendReadMessage" @update:modelValue="onChangeSendReadMessage">
<template #label>{{ i18n.ts.sendPushNotificationReadMessage }}</template>
<template #caption>
<I18n :src="i18n.ts.sendPushNotificationReadMessageCaption">
<template #emptyPushNotificationMessage>{{ i18n.ts._notification.emptyPushNotificationMessage }}</template>
</I18n>
</template>
<template #caption>{{ i18n.ts.sendPushNotificationReadMessageCaption }}</template>
</MkSwitch>
</div>
</FormSection>

View File

@ -40,7 +40,7 @@ async function install() {
code.value = null;
router.push('/settings/plugin');
} catch (err) {
} catch (err: any) {
os.alert({
type: 'error',
title: 'Install failed',

View File

@ -40,6 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import MkInput from '@/components/MkInput.vue';
import FormSection from '@/components/form/section.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@ -61,7 +62,7 @@ const event_reaction = ref(true);
const event_mention = ref(true);
async function create(): Promise<void> {
const events: string[] = [];
const events = [] as Misskey.entities.UserWebhook['on'];
if (event_follow.value) events.push('follow');
if (event_followed.value) events.push('followed');
if (event_note.value) events.push('note');

View File

@ -34,7 +34,7 @@ const props = withDefaults(defineProps<{
limit: 50,
});
const chartSrc = ref('per-user-notes');
const chartSrc = ref<'per-user-notes' | 'per-user-pv'>('per-user-notes');
function showMenu(ev: MouseEvent) {
os.popupMenu([{

View File

@ -53,7 +53,7 @@ function getInstanceIcon(instance: Misskey.entities.FederationInstance): string
misskeyApiGet('federation/instances', {
sort: '+pubSub',
limit: 20,
blocked: 'false',
blocked: false,
}).then(_instances => {
instances.value = _instances;
});

View File

@ -469,6 +469,8 @@ export class PreferencesManager {
return local;
} else if (choice === 'merge') {
return mergedValue!;
} else { // TSを黙らすため
return undefined;
}
}

View File

@ -57,6 +57,7 @@ import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
import { $i } from '@/i.js';
import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
const WINDOW_THRESHOLD = 1400;
@ -72,8 +73,11 @@ const otherNavItemIndicated = computed<boolean>(() => {
});
async function more(ev: MouseEvent) {
const target = getHTMLElementOrNull(ev.currentTarget ?? ev.target);
if (!target) return;
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkLaunchPad.vue').then(x => x.default), {
anchorElement: ev.currentTarget ?? ev.target,
anchorElement: target,
anchor: { x: 'center', y: 'bottom' },
}, {
closed: () => dispose(),

View File

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
mode="default"
>
<MkMarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse">
<span v-for="instance in instances" :key="instance.id" :class="[$style.item, { [$style.colored]: colored }]" :style="{ background: colored ? instance.themeColor : '' }">
<span v-for="instance in instances" :key="instance.id" :class="[$style.item, { [$style.colored]: colored }]" :style="{ background: colored ? instance.themeColor ?? '' : '' }">
<img :class="$style.icon" :src="getInstanceIcon(instance)" alt=""/>
<MkA :to="`/instance-info/${instance.host}`" :class="$style.host" class="_monospace">
{{ instance.host }}

View File

@ -300,15 +300,13 @@ export async function createCroppedImageDriveFileFromImageDriveFile(imageDriveFi
});
}
export async function selectDriveFolder(initialFolder: Misskey.entities.DriveFolder['id'] | null): Promise<Misskey.entities.DriveFolder[]> {
export async function selectDriveFolder(initialFolder: Misskey.entities.DriveFolder['id'] | null): Promise<(Misskey.entities.DriveFolder | null)[]> {
return new Promise(async resolve => {
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkDriveFolderSelectDialog.vue').then(x => x.default), {
initialFolder,
}, {
done: folders => {
if (folders) {
resolve(folders);
}
resolve(folders);
},
closed: () => dispose(),
});

View File

@ -9,7 +9,7 @@ import * as mfm from 'mfm-js';
export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] {
// TODO: 重複を削除
const mentionNodes = mfm.extract(nodes, (node) => node.type === 'mention');
const mentionNodes = mfm.extract(nodes, (node) => node.type === 'mention') as mfm.MfmMention[];
const mentions = mentionNodes.map(x => x.props);
return mentions;

View File

@ -15,7 +15,7 @@ import 'intersection-observer';
describe('XHome', () => {
const renderHome = (user: Partial<Misskey.entities.UserDetailed>): RenderResult => {
return render(XHome, {
props: { user, disableNotes: true },
props: { user: user as Misskey.entities.UserDetailed, disableNotes: true },
global: { directives, components },
});
};

View File

@ -29,7 +29,7 @@ describe('MkMediaImage', () => {
comment: null,
properties: {},
...image,
} as DriveFile,
} as Misskey.entities.DriveFile,
},
global: { directives, components },
});

View File

@ -55,6 +55,7 @@
"./@types/**/*.ts"
],
"exclude": [
".storybook/**/*"
".storybook/**/*",
"./src/**/*.stories.ts"
]
}

View File

@ -2151,6 +2151,7 @@ declare namespace entities {
Note,
NoteDraft,
NoteReaction,
NoteReactionWithNote,
NoteFavorite,
Notification_2 as Notification,
DriveFile,
@ -2192,6 +2193,7 @@ declare namespace entities {
MetaLite,
MetaDetailedOnly,
MetaDetailed,
UserWebhook,
SystemWebhook,
AbuseReportNotificationRecipient,
ChatMessage,
@ -2807,7 +2809,7 @@ type ModerationLog = {
id: ID;
createdAt: DateString;
userId: User['id'];
user: UserDetailedNotMe | null;
user: UserDetailedNotMe;
} & ({
type: 'updateServerSettings';
info: ModerationLogPayloads['updateServerSettings'];
@ -2961,10 +2963,13 @@ type ModerationLog = {
} | {
type: 'deleteChatRoom';
info: ModerationLogPayloads['deleteChatRoom'];
} | {
type: 'updateProxyAccountDescription';
info: ModerationLogPayloads['updateProxyAccountDescription'];
});
// @public (undocumented)
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "forwardAbuseReport", "updateAbuseReportNote", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost", "deleteChatRoom"];
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "forwardAbuseReport", "updateAbuseReportNote", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost", "deleteChatRoom", "updateProxyAccountDescription"];
// @public (undocumented)
type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];
@ -3009,6 +3014,9 @@ type NoteFavorite = components['schemas']['NoteFavorite'];
// @public (undocumented)
type NoteReaction = components['schemas']['NoteReaction'];
// @public (undocumented)
type NoteReactionWithNote = components['schemas']['NoteReactionWithNote'];
// @public (undocumented)
type NotesChildrenRequest = operations['notes___children']['requestBody']['content']['application/json'];
@ -3806,6 +3814,9 @@ type UsersShowResponse = operations['users___show']['responses']['200']['content
// @public (undocumented)
type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody']['content']['application/json'];
// @public (undocumented)
type UserWebhook = components['schemas']['UserWebhook'];
// @public (undocumented)
type V2AdminEmojiListRequest = operations['v2___admin___emoji___list']['requestBody']['content']['application/json'];

View File

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2025.8.0-beta.4",
"version": "2025.8.0-beta.6",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",

View File

@ -16,6 +16,7 @@ export type App = components['schemas']['App'];
export type Note = components['schemas']['Note'];
export type NoteDraft = components['schemas']['NoteDraft'];
export type NoteReaction = components['schemas']['NoteReaction'];
export type NoteReactionWithNote = components['schemas']['NoteReactionWithNote'];
export type NoteFavorite = components['schemas']['NoteFavorite'];
export type Notification = components['schemas']['Notification'];
export type DriveFile = components['schemas']['DriveFile'];
@ -57,6 +58,7 @@ export type ReversiGameDetailed = components['schemas']['ReversiGameDetailed'];
export type MetaLite = components['schemas']['MetaLite'];
export type MetaDetailedOnly = components['schemas']['MetaDetailedOnly'];
export type MetaDetailed = components['schemas']['MetaDetailed'];
export type UserWebhook = components['schemas']['UserWebhook'];
export type SystemWebhook = components['schemas']['SystemWebhook'];
export type AbuseReportNotificationRecipient = components['schemas']['AbuseReportNotificationRecipient'];
export type ChatMessage = components['schemas']['ChatMessage'];

View File

@ -3949,7 +3949,7 @@ export type components = {
*/
host: string | null;
/** Format: url */
avatarUrl: string | null;
avatarUrl: string;
avatarBlurhash: string | null;
avatarDecorations: {
/** Format: id */
@ -4058,8 +4058,8 @@ export type components = {
/** Format: id */
bannerId: string | null;
followedMessage: string | null;
isModerator: boolean | null;
isAdmin: boolean | null;
isModerator: boolean;
isAdmin: boolean;
injectFeaturedNote: boolean;
receiveAnnouncementEmail: boolean;
alwaysMarkNsfw: boolean;
@ -4085,7 +4085,7 @@ export type components = {
unreadNotificationsCount: number;
mutedWords: string[][];
hardMutedWords: string[][];
mutedInstances: string[] | null;
mutedInstances: string[];
notificationRecieveConfig: {
note?: {
/** @enum {string} */
@ -4454,16 +4454,22 @@ export type components = {
reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null;
};
NoteReaction: {
/**
* Format: id
* @example xxxxxxxxxx
*/
/** Format: id */
id: string;
/** Format: date-time */
createdAt: string;
user: components['schemas']['UserLite'];
type: string;
};
NoteReactionWithNote: {
/** Format: id */
id: string;
/** Format: date-time */
createdAt: string;
user: components['schemas']['UserLite'];
type: string;
note: components['schemas']['Note'];
};
NoteFavorite: {
/**
* Format: id
@ -5437,6 +5443,20 @@ export type components = {
cacheRemoteSensitiveFiles: boolean;
};
MetaDetailed: components['schemas']['MetaLite'] & components['schemas']['MetaDetailedOnly'];
UserWebhook: {
/** Format: id */
id: string;
/** Format: id */
userId: string;
name: string;
on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[];
url: string;
secret: string;
active: boolean;
/** Format: date-time */
latestSentAt: string | null;
latestStatus: number | null;
};
SystemWebhook: {
id: string;
isActive: boolean;
@ -6694,6 +6714,13 @@ export interface operations {
updatedAt: string | null;
text: string;
title: string;
icon: string | null;
display: string;
isActive: boolean;
forExistingUsers: boolean;
silence: boolean;
needConfirmationToRead: boolean;
userId: string | null;
imageUrl: string | null;
reads: number;
}[];
@ -7655,6 +7682,10 @@ export interface operations {
folderId: string | null;
isSensitive: boolean;
isLink: boolean;
maybeSensitive: boolean;
maybePorn: boolean;
requestIp: string | null;
requestHeaders: Record<string, never> | null;
};
};
};
@ -9298,8 +9329,10 @@ export interface operations {
mcaptchaSecretKey: string | null;
recaptchaSecretKey: string | null;
turnstileSecretKey: string | null;
sensitiveMediaDetection: string;
sensitiveMediaDetectionSensitivity: string;
/** @enum {string} */
sensitiveMediaDetection: 'none' | 'all' | 'local' | 'remote';
/** @enum {string} */
sensitiveMediaDetectionSensitivity: 'medium' | 'low' | 'high' | 'veryLow' | 'veryHigh';
setSensitiveFlagAutomatically: boolean;
enableSensitiveMediaDetectionForVideos: boolean;
/** Format: id */
@ -9362,6 +9395,7 @@ export interface operations {
privacyPolicyUrl: string | null;
inquiryUrl: string | null;
repositoryUrl: string | null;
feedbackUrl: string | null;
/**
* @deprecated
* @description [Deprecated] Use "urlPreviewSummaryProxyUrl" instead.
@ -24401,6 +24435,8 @@ export interface operations {
/** Format: date-time */
lastUsedAt?: string;
permission: string[];
iconUrl?: string | null;
description?: string | null;
}[];
};
};
@ -27636,20 +27672,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
'application/json': {
/** Format: misskey:id */
id: string;
/** Format: misskey:id */
userId: string;
name: string;
on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[];
url: string;
secret: string;
active: boolean;
/** Format: date-time */
latestSentAt: string | null;
latestStatus: number | null;
}[];
'application/json': components['schemas']['UserWebhook'][];
};
};
/** @description Client error */
@ -27715,20 +27738,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
'application/json': {
/** Format: misskey:id */
id: string;
/** Format: misskey:id */
userId: string;
name: string;
on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[];
url: string;
secret: string;
active: boolean;
/** Format: date-time */
latestSentAt: string | null;
latestStatus: number | null;
};
'application/json': components['schemas']['UserWebhook'];
};
};
/** @description Client error */
@ -35752,7 +35762,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['NoteReaction'][];
'application/json': components['schemas']['NoteReactionWithNote'][];
};
};
/** @description Client error */

View File

@ -167,6 +167,7 @@ export const moderationLogTypes = [
'deleteFlash',
'deleteGalleryPost',
'deleteChatRoom',
'updateProxyAccountDescription',
] as const;
export const queueTypes = [
@ -193,7 +194,15 @@ export const reversiUpdateKeys = [
export type ReversiUpdateKey = typeof reversiUpdateKeys[number];
type AvatarDecoration = UserLite['avatarDecorations'][number];
type AvatarDecoration = {
id: string;
name: string;
url: string;
angle?: number;
flipH?: boolean;
offsetX?: number;
offsetY?: number;
};
type ReceivedAbuseReport = {
reportId: AbuseReportNotificationRecipient['id'];
@ -455,4 +464,8 @@ export type ModerationLogPayloads = {
roomId: string;
room: ChatRoom;
};
updateProxyAccountDescription: {
before: string | null;
after: string | null;
}
};

View File

@ -49,7 +49,7 @@ export type ModerationLog = {
id: ID;
createdAt: DateString;
userId: User['id'];
user: UserDetailedNotMe | null;
user: UserDetailedNotMe;
} & ({
type: 'updateServerSettings';
info: ModerationLogPayloads['updateServerSettings'];
@ -203,6 +203,9 @@ export type ModerationLog = {
} | {
type: 'deleteChatRoom';
info: ModerationLogPayloads['deleteChatRoom'];
} | {
type: 'updateProxyAccountDescription';
info: ModerationLogPayloads['updateProxyAccountDescription'];
});
export type ServerStats = {