Merge branch 'develop' into fix-mfm-doesnt-work-in-some-places

This commit is contained in:
zyoshoka 2024-08-17 12:46:36 +09:00
commit 394f6c09cb
No known key found for this signature in database
36 changed files with 379 additions and 200 deletions

View File

@ -1,8 +1,9 @@
## 2024.8.0 ## 2024.8.0
### General ### General
- Fix: リモートユーザのフォロー・フォロワーの一覧が非公開設定の場合も表示できてしまう問題を修正
- Enhance: モデレーターはすべてのユーザーのフォロー・フォロワーの一覧を見られるように - Enhance: モデレーターはすべてのユーザーのフォロー・フォロワーの一覧を見られるように
- Enhance: アカウントの削除のモデレーションログを残すように
- Fix: リモートユーザのフォロー・フォロワーの一覧が非公開設定の場合も表示できてしまう問題を修正
### Client ### Client
- Enhance: 「自分のPlay」ページにおいてPlayが非公開かどうかが一目でわかるように - Enhance: 「自分のPlay」ページにおいてPlayが非公開かどうかが一目でわかるように
@ -10,9 +11,12 @@
- Fix: ページ遷移に失敗することがある問題を修正 - Fix: ページ遷移に失敗することがある問題を修正
- Fix: iOSでユーザー名などがリンクとして誤検知される現象を抑制 - Fix: iOSでユーザー名などがリンクとして誤検知される現象を抑制
- Fix: mCaptchaを使用していてもbotプロテクションに関する警告が消えないのを修正 - Fix: mCaptchaを使用していてもbotプロテクションに関する警告が消えないのを修正
- Fix: ユーザーのモデレーションページにおいてユーザー名にドットが入っているとシステムアカウントとして表示されてしまう問題を修正
- Fix: 特定の条件下でノートの削除ボタンが出ないのを修正
- Fix: MFMが機能すべき箇所で機能していないところがある問題を修正 - Fix: MFMが機能すべき箇所で機能していないところがある問題を修正
### Server ### Server
- Enhance: 凍結されたアカウントのフォローリクエストを表示しないように
- Fix: WSの`readAllNotifications` メッセージが `body` を持たない場合に動作しない問題 #14374 - Fix: WSの`readAllNotifications` メッセージが `body` を持たない場合に動作しない問題 #14374
- 通知ページや通知カラム(デッキ)を開いている状態において、新たに発生した通知が既読されない問題が修正されます。 - 通知ページや通知カラム(デッキ)を開いている状態において、新たに発生した通知が既読されない問題が修正されます。
- これにより、プッシュ通知が有効な同条件下の環境において、プッシュ通知が常に発生してしまう問題も修正されます。 - これにより、プッシュ通知が有効な同条件下の環境において、プッシュ通知が常に発生してしまう問題も修正されます。
@ -23,6 +27,10 @@
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/679) (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/679)
- Fix: ActivityPubのエンティティタイプ判定で不明なタイプを受け取った場合でも処理を継続するように - Fix: ActivityPubのエンティティタイプ判定で不明なタイプを受け取った場合でも処理を継続するように
- キュー処理のつまりが改善される可能性があります - キュー処理のつまりが改善される可能性があります
- Fix: リバーシの対局設定の変更が反映されないのを修正
- Fix: 無制限にストリーミングのチャンネルに接続できる問題を修正
- Fix: ベースロールのポリシーを変更した際にモデログに記録されないのを修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/700)
## 2024.7.0 ## 2024.7.0

View File

@ -182,7 +182,7 @@ addAccount: "Add account"
reloadAccountsList: "Reload account list" reloadAccountsList: "Reload account list"
loginFailed: "Failed to sign in" loginFailed: "Failed to sign in"
showOnRemote: "View on remote instance" showOnRemote: "View on remote instance"
continueOnRemote: "リモートで続行" continueOnRemote: "Continue on a remote server"
chooseServerOnMisskeyHub: "Choose a server from the Misskey Hub" chooseServerOnMisskeyHub: "Choose a server from the Misskey Hub"
specifyServerHost: "Specify a server host directly" specifyServerHost: "Specify a server host directly"
inputHostName: "Enter the domain" inputHostName: "Enter the domain"

View File

@ -60,6 +60,7 @@ copyFileId: "Copiar ID del archivo"
copyFolderId: "Copiar ID de carpeta" copyFolderId: "Copiar ID de carpeta"
copyProfileUrl: "Copiar la URL del perfil" copyProfileUrl: "Copiar la URL del perfil"
searchUser: "Buscar un usuario" searchUser: "Buscar un usuario"
searchThisUsersNotes: ""
reply: "Responder" reply: "Responder"
loadMore: "Ver más" loadMore: "Ver más"
showMore: "Ver más" showMore: "Ver más"

8
locales/index.d.ts vendored
View File

@ -8985,6 +8985,10 @@ export interface Locale extends ILocale {
* *
*/ */
"chooseBlock": string; "chooseBlock": string;
/**
*
*/
"enterSectionTitle": string;
/** /**
* *
*/ */
@ -9679,6 +9683,10 @@ export interface Locale extends ILocale {
* *
*/ */
"deleteAbuseReportNotificationRecipient": string; "deleteAbuseReportNotificationRecipient": string;
/**
*
*/
"deleteAccount": string;
}; };
"_fileViewer": { "_fileViewer": {
/** /**

View File

@ -2371,6 +2371,7 @@ _pages:
eyeCatchingImageSet: "アイキャッチ画像を設定" eyeCatchingImageSet: "アイキャッチ画像を設定"
eyeCatchingImageRemove: "アイキャッチ画像を削除" eyeCatchingImageRemove: "アイキャッチ画像を削除"
chooseBlock: "ブロックを追加" chooseBlock: "ブロックを追加"
enterSectionTitle: "セクションタイトルを入力"
selectType: "種類を選択" selectType: "種類を選択"
contentBlocks: "コンテンツ" contentBlocks: "コンテンツ"
inputBlocks: "入力" inputBlocks: "入力"
@ -2567,6 +2568,7 @@ _moderationLogTypes:
createAbuseReportNotificationRecipient: "通報の通知先を作成" createAbuseReportNotificationRecipient: "通報の通知先を作成"
updateAbuseReportNotificationRecipient: "通報の通知先を更新" updateAbuseReportNotificationRecipient: "通報の通知先を更新"
deleteAbuseReportNotificationRecipient: "通報の通知先を削除" deleteAbuseReportNotificationRecipient: "通報の通知先を削除"
deleteAccount: "アカウントを削除"
_fileViewer: _fileViewer:
title: "ファイルの詳細" title: "ファイルの詳細"

View File

@ -1675,7 +1675,7 @@ _role:
descriptionOfPermission: "<b>Moderador</b> permite que você execute operações básicas relacionadas à moderação.\n<b>Administradores</b> podem alterar todas as configurações do servidor." descriptionOfPermission: "<b>Moderador</b> permite que você execute operações básicas relacionadas à moderação.\n<b>Administradores</b> podem alterar todas as configurações do servidor."
assignTarget: "Atribuir" assignTarget: "Atribuir"
descriptionOfAssignTarget: "<b>Manual</b> para gerenciar manualmente quem está incluído neste cargo.\n<b>Condicional</b> define uma condição e os usuários que corresponderem a ela serão incluídos automaticamente." descriptionOfAssignTarget: "<b>Manual</b> para gerenciar manualmente quem está incluído neste cargo.\n<b>Condicional</b> define uma condição e os usuários que corresponderem a ela serão incluídos automaticamente."
manual: "Documentação" manual: "Manual"
manualRoles: "Cargos manuais" manualRoles: "Cargos manuais"
conditional: "Condicional" conditional: "Condicional"
conditionalRoles: "Cargos condicionais" conditionalRoles: "Cargos condicionais"

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2024.8.0-alpha.0", "version": "2024.8.0-alpha.1",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
@ -61,7 +61,7 @@
"glob": "11.0.0" "glob": "11.0.0"
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/eslint-plugin": "2.0.2", "@misskey-dev/eslint-plugin": "2.0.3",
"@types/node": "20.14.12", "@types/node": "20.14.12",
"@typescript-eslint/eslint-plugin": "7.17.0", "@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0", "@typescript-eslint/parser": "7.17.0",

View File

@ -4,12 +4,15 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository } from '@/models/_.js'; import { Not, IsNull } from 'typeorm';
import type { FollowingsRepository, MiUser, UsersRepository } from '@/models/_.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { UserSuspendService } from '@/core/UserSuspendService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@Injectable() @Injectable()
export class DeleteAccountService { export class DeleteAccountService {
@ -17,9 +20,14 @@ export class DeleteAccountService {
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
private userSuspendService: UserSuspendService, @Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private userEntityService: UserEntityService,
private apRendererService: ApRendererService,
private queueService: QueueService, private queueService: QueueService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private moderationLogService: ModerationLogService,
) { ) {
} }
@ -27,16 +35,52 @@ export class DeleteAccountService {
public async deleteAccount(user: { public async deleteAccount(user: {
id: string; id: string;
host: string | null; host: string | null;
}): Promise<void> { }, moderator?: MiUser): Promise<void> {
const _user = await this.usersRepository.findOneByOrFail({ id: user.id }); const _user = await this.usersRepository.findOneByOrFail({ id: user.id });
if (_user.isRoot) throw new Error('cannot delete a root account'); if (_user.isRoot) throw new Error('cannot delete a root account');
if (moderator != null) {
this.moderationLogService.log(moderator, 'deleteAccount', {
userId: user.id,
userUsername: _user.username,
userHost: user.host,
});
}
// 物理削除する前にDelete activityを送信する // 物理削除する前にDelete activityを送信する
await this.userSuspendService.doPostSuspend(user).catch(e => {}); if (this.userEntityService.isLocalUser(user)) {
// 知り得る全SharedInboxにDelete配信
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
const queue: string[] = [];
const followings = await this.followingsRepository.find({
where: [
{ followerSharedInbox: Not(IsNull()) },
{ followeeSharedInbox: Not(IsNull()) },
],
select: ['followerSharedInbox', 'followeeSharedInbox'],
});
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
for (const inbox of inboxes) {
if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
}
for (const inbox of queue) {
this.queueService.deliver(user, content, inbox, true);
}
this.queueService.createDeleteAccountJob(user, { this.queueService.createDeleteAccountJob(user, {
soft: false, soft: false,
}); });
} else {
// リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する
this.queueService.createDeleteAccountJob(user, {
soft: true,
});
}
await this.usersRepository.update(user.id, { await this.usersRepository.update(user.id, {
isDeleted: true, isDeleted: true,

View File

@ -6,6 +6,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import { reversiUpdateKeys } from 'misskey-js';
import * as Reversi from 'misskey-reversi'; import * as Reversi from 'misskey-reversi';
import { IsNull, LessThan, MoreThan } from 'typeorm'; import { IsNull, LessThan, MoreThan } from 'typeorm';
import type { import type {
@ -399,7 +400,33 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
} }
@bindThis @bindThis
public async updateSettings(gameId: MiReversiGame['id'], user: MiUser, key: string, value: any) { public isValidReversiUpdateKey(key: unknown): key is typeof reversiUpdateKeys[number] {
if (typeof key !== 'string') return false;
return (reversiUpdateKeys as string[]).includes(key);
}
@bindThis
public isValidReversiUpdateValue<K extends typeof reversiUpdateKeys[number]>(key: K, value: unknown): value is MiReversiGame[K] {
switch (key) {
case 'map':
return Array.isArray(value) && value.every(row => typeof row === 'string');
case 'bw':
return typeof value === 'string' && ['random', '1', '2'].includes(value);
case 'isLlotheo':
return typeof value === 'boolean';
case 'canPutEverywhere':
return typeof value === 'boolean';
case 'loopedBoard':
return typeof value === 'boolean';
case 'timeLimitForEachTurn':
return typeof value === 'number' && value >= 0;
default:
return false;
}
}
@bindThis
public async updateSettings<K extends typeof reversiUpdateKeys[number]>(gameId: MiReversiGame['id'], user: MiUser, key: K, value: MiReversiGame[K]) {
const game = await this.get(gameId); const game = await this.get(gameId);
if (game == null) throw new Error('game not found'); if (game == null) throw new Error('game not found');
if (game.isStarted) return; if (game.isStarted) return;
@ -407,10 +434,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
if ((game.user1Id === user.id) && game.user1Ready) return; if ((game.user1Id === user.id) && game.user1Ready) return;
if ((game.user2Id === user.id) && game.user2Ready) return; if ((game.user2Id === user.id) && game.user2Ready) return;
if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard', 'timeLimitForEachTurn'].includes(key)) return;
// TODO: より厳格なバリデーション
const updatedGame = { const updatedGame = {
...game, ...game,
[key]: value, [key]: value,

View File

@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Not, IsNull } from 'typeorm'; import { Not, IsNull } from 'typeorm';
import type { FollowingsRepository } from '@/models/_.js'; import type { FollowingsRepository, FollowRequestsRepository, UsersRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
@ -13,24 +13,75 @@ import { DI } from '@/di-symbols.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RelationshipJobData } from '@/queue/types.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@Injectable() @Injectable()
export class UserSuspendService { export class UserSuspendService {
constructor( constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository) @Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository, private followingsRepository: FollowingsRepository,
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private queueService: QueueService, private queueService: QueueService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private moderationLogService: ModerationLogService,
) { ) {
} }
@bindThis @bindThis
public async doPostSuspend(user: { id: MiUser['id']; host: MiUser['host'] }): Promise<void> { public async suspend(user: MiUser, moderator: MiUser): Promise<void> {
await this.usersRepository.update(user.id, {
isSuspended: true,
});
this.moderationLogService.log(moderator, 'suspend', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
});
(async () => {
await this.postSuspend(user).catch(e => {});
await this.unFollowAll(user).catch(e => {});
})();
}
@bindThis
public async unsuspend(user: MiUser, moderator: MiUser): Promise<void> {
await this.usersRepository.update(user.id, {
isSuspended: false,
});
this.moderationLogService.log(moderator, 'unsuspend', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
});
(async () => {
await this.postUnsuspend(user).catch(e => {});
})();
}
@bindThis
private async postSuspend(user: { id: MiUser['id']; host: MiUser['host'] }): Promise<void> {
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true }); this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });
this.followRequestsRepository.delete({
followeeId: user.id,
});
this.followRequestsRepository.delete({
followerId: user.id,
});
if (this.userEntityService.isLocalUser(user)) { if (this.userEntityService.isLocalUser(user)) {
// 知り得る全SharedInboxにDelete配信 // 知り得る全SharedInboxにDelete配信
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user)); const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
@ -58,7 +109,7 @@ export class UserSuspendService {
} }
@bindThis @bindThis
public async doPostUnsuspend(user: MiUser): Promise<void> { private async postUnsuspend(user: MiUser): Promise<void> {
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false }); this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false });
if (this.userEntityService.isLocalUser(user)) { if (this.userEntityService.isLocalUser(user)) {
@ -86,4 +137,26 @@ export class UserSuspendService {
} }
} }
} }
@bindThis
private async unFollowAll(follower: MiUser) {
const followings = await this.followingsRepository.find({
where: {
followerId: follower.id,
followeeId: Not(IsNull()),
},
});
const jobs: RelationshipJobData[] = [];
for (const following of followings) {
if (following.followeeId && following.followerId) {
jobs.push({
from: { id: following.followerId },
to: { id: following.followeeId },
silent: true,
});
}
}
this.queueService.createUnfollowJob(jobs);
}
} }

View File

@ -7,9 +7,9 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js'; import type { UsersRepository } from '@/models/_.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { UserSuspendService } from '@/core/UserSuspendService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DeleteAccountService } from '@/core/DeleteAccountService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -33,9 +33,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
private userEntityService: UserEntityService, private deleteAccoountService: DeleteAccountService,
private queueService: QueueService,
private userSuspendService: UserSuspendService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId }); const user = await this.usersRepository.findOneBy({ id: ps.userId });
@ -48,22 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('cannot delete a root account'); throw new Error('cannot delete a root account');
} }
if (this.userEntityService.isLocalUser(user)) { await this.deleteAccoountService.deleteAccount(user);
// 物理削除する前にDelete activityを送信する
await this.userSuspendService.doPostSuspend(user).catch(err => {});
this.queueService.createDeleteAccountJob(user, {
soft: false,
});
} else {
this.queueService.createDeleteAccountJob(user, {
soft: true, // リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する
});
}
await this.usersRepository.update(user.id, {
isDeleted: true,
});
}); });
} }
} }

View File

@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = { export const meta = {
tags: ['admin', 'role'], tags: ['admin', 'role'],
@ -33,12 +34,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor( constructor(
private metaService: MetaService, private metaService: MetaService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private moderationLogService: ModerationLogService,
) { ) {
super(meta, paramDef, async (ps) => { super(meta, paramDef, async (ps, me) => {
const before = await this.metaService.fetch(true);
await this.metaService.update({ await this.metaService.update({
policies: ps.policies, policies: ps.policies,
}); });
this.globalEventService.publishInternalEvent('policiesUpdated', ps.policies);
const after = await this.metaService.fetch(true);
this.globalEventService.publishInternalEvent('policiesUpdated', after.policies);
this.moderationLogService.log(me, 'updateServerSettings', {
before: before.policies,
after: after.policies,
});
}); });
} }
} }

View File

@ -3,18 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { IsNull, Not } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, FollowingsRepository } from '@/models/_.js'; import type { UsersRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import type { RelationshipJobData } from '@/queue/types.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { UserSuspendService } from '@/core/UserSuspendService.js'; import { UserSuspendService } from '@/core/UserSuspendService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { QueueService } from '@/core/QueueService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -38,13 +32,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private userSuspendService: UserSuspendService, private userSuspendService: UserSuspendService,
private roleService: RoleService, private roleService: RoleService,
private moderationLogService: ModerationLogService,
private queueService: QueueService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId }); const user = await this.usersRepository.findOneBy({ id: ps.userId });
@ -57,42 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('cannot suspend moderator account'); throw new Error('cannot suspend moderator account');
} }
await this.usersRepository.update(user.id, { await this.userSuspendService.suspend(user, me);
isSuspended: true,
});
this.moderationLogService.log(me, 'suspend', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
});
(async () => {
await this.userSuspendService.doPostSuspend(user).catch(e => {});
await this.unFollowAll(user).catch(e => {});
})();
});
}
@bindThis
private async unFollowAll(follower: MiUser) {
const followings = await this.followingsRepository.find({
where: {
followerId: follower.id,
followeeId: Not(IsNull()),
},
});
const jobs: RelationshipJobData[] = [];
for (const following of followings) {
if (following.followeeId && following.followerId) {
jobs.push({
from: { id: following.followerId },
to: { id: following.followeeId },
silent: true,
}); });
} }
} }
this.queueService.createUnfollowJob(jobs);
}
}

View File

@ -6,7 +6,6 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js'; import type { UsersRepository } from '@/models/_.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { UserSuspendService } from '@/core/UserSuspendService.js'; import { UserSuspendService } from '@/core/UserSuspendService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -33,7 +32,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
private userSuspendService: UserSuspendService, private userSuspendService: UserSuspendService,
private moderationLogService: ModerationLogService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId }); const user = await this.usersRepository.findOneBy({ id: ps.userId });
@ -42,17 +40,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('user not found'); throw new Error('user not found');
} }
await this.usersRepository.update(user.id, { await this.userSuspendService.unsuspend(user, me);
isSuspended: false,
});
this.moderationLogService.log(me, 'unsuspend', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
});
this.userSuspendService.doPostUnsuspend(user);
}); });
} }
} }

View File

@ -20,6 +20,8 @@ import type { ChannelsService } from './ChannelsService.js';
import type { EventEmitter } from 'events'; import type { EventEmitter } from 'events';
import type Channel from './channel.js'; import type Channel from './channel.js';
const MAX_CHANNELS_PER_CONNECTION = 32;
/** /**
* Main stream connection * Main stream connection
*/ */
@ -255,6 +257,10 @@ export default class Connection {
*/ */
@bindThis @bindThis
public connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) { public connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) {
if (this.channels.length >= MAX_CHANNELS_PER_CONNECTION) {
return;
}
const channelService = this.channelsService.getChannelService(channel); const channelService = this.channelsService.getChannelService(channel);
if (channelService.requireCredential && this.user == null) { if (channelService.requireCredential && this.user == null) {

View File

@ -12,6 +12,7 @@ import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityServi
import { isJsonObject } from '@/misc/json-value.js'; import { isJsonObject } from '@/misc/json-value.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js'; import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
import { reversiUpdateKeys } from 'misskey-js';
class ReversiGameChannel extends Channel { class ReversiGameChannel extends Channel {
public readonly chName = 'reversiGame'; public readonly chName = 'reversiGame';
@ -46,8 +47,9 @@ class ReversiGameChannel extends Channel {
break; break;
case 'updateSettings': case 'updateSettings':
if (!isJsonObject(body)) return; if (!isJsonObject(body)) return;
if (typeof body.key !== 'string') return; if (!this.reversiService.isValidReversiUpdateKey(body.key)) return;
if (!isJsonObject(body.value)) return; if (!this.reversiService.isValidReversiUpdateValue(body.key, body.value)) return;
this.updateSettings(body.key, body.value); this.updateSettings(body.key, body.value);
break; break;
case 'cancel': case 'cancel':
@ -64,7 +66,7 @@ class ReversiGameChannel extends Channel {
} }
@bindThis @bindThis
private async updateSettings(key: string, value: JsonObject) { private async updateSettings<K extends typeof reversiUpdateKeys[number]>(key: K, value: MiReversiGame[K]) {
if (this.user == null) return; if (this.user == null) return;
this.reversiService.updateSettings(this.gameId!, this.user, key, value); this.reversiService.updateSettings(this.gameId!, this.user, key, value);

View File

@ -96,6 +96,7 @@ export const moderationLogTypes = [
'createAbuseReportNotificationRecipient', 'createAbuseReportNotificationRecipient',
'updateAbuseReportNotificationRecipient', 'updateAbuseReportNotificationRecipient',
'deleteAbuseReportNotificationRecipient', 'deleteAbuseReportNotificationRecipient',
'deleteAccount',
] as const; ] as const;
export type ModerationLogPayloads = { export type ModerationLogPayloads = {
@ -314,6 +315,11 @@ export type ModerationLogPayloads = {
recipientId: string; recipientId: string;
recipient: any; recipient: any;
}; };
deleteAccount: {
userId: string;
userUsername: string;
userHost: string | null;
};
}; };
export type Serialized<T> = { export type Serialized<T> = {

View File

@ -231,17 +231,18 @@ export async function mainBoot() {
claimAchievement('client60min'); claimAchievement('client60min');
}, 1000 * 60 * 60); }, 1000 * 60 * 60);
const lastUsed = miLocalStorage.getItem('lastUsed'); // 邪魔
if (lastUsed) { //const lastUsed = miLocalStorage.getItem('lastUsed');
const lastUsedDate = parseInt(lastUsed, 10); //if (lastUsed) {
// 二時間以上前なら // const lastUsedDate = parseInt(lastUsed, 10);
if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) { // // 二時間以上前なら
toast(i18n.tsx.welcomeBackWithName({ // if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
name: $i.name || $i.username, // toast(i18n.tsx.welcomeBackWithName({
})); // name: $i.name || $i.username,
} // }));
} // }
miLocalStorage.setItem('lastUsed', Date.now().toString()); //}
//miLocalStorage.setItem('lastUsed', Date.now().toString());
const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt'); const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo'); const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');

View File

@ -200,6 +200,7 @@ import { host } from '@/config.js';
import { isEnabledUrlPreview } from '@/instance.js'; import { isEnabledUrlPreview } from '@/instance.js';
import { type Keymap } from '@/scripts/hotkey.js'; import { type Keymap } from '@/scripts/hotkey.js';
import { focusPrev, focusNext } from '@/scripts/focus.js'; import { focusPrev, focusNext } from '@/scripts/focus.js';
import { getAppearNote } from '@/scripts/get-appear-note.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;
@ -242,14 +243,7 @@ if (noteViewInterruptors.length > 0) {
}); });
} }
const isRenote = ( const isRenote = Misskey.note.isPureRenote(note.value);
note.value.renote != null &&
note.value.reply == null &&
note.value.text == null &&
note.value.cw == null &&
note.value.fileIds && note.value.fileIds.length === 0 &&
note.value.poll == null
);
const rootEl = shallowRef<HTMLElement>(); const rootEl = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>();
@ -257,7 +251,7 @@ const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>();
const reactButton = shallowRef<HTMLElement>(); const reactButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>(); const clipButton = shallowRef<HTMLElement>();
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); const appearNote = computed(() => getAppearNote(note.value));
const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>(); const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
const isMyRenote = $i && ($i.id === note.value.userId); const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(false); const showContent = ref(false);

View File

@ -235,6 +235,7 @@ import MkPagination, { type Paging } from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { isEnabledUrlPreview } from '@/instance.js'; import { isEnabledUrlPreview } from '@/instance.js';
import { getAppearNote } from '@/scripts/get-appear-note.js';
import { type Keymap } from '@/scripts/hotkey.js'; import { type Keymap } from '@/scripts/hotkey.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
@ -267,14 +268,7 @@ if (noteViewInterruptors.length > 0) {
}); });
} }
const isRenote = ( const isRenote = Misskey.note.isPureRenote(note.value);
note.value.renote != null &&
note.value.reply == null &&
note.value.text == null &&
note.value.cw == null &&
note.value.fileIds && note.value.fileIds.length === 0 &&
note.value.poll == null
);
const rootEl = shallowRef<HTMLElement>(); const rootEl = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>();
@ -282,7 +276,7 @@ const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>();
const reactButton = shallowRef<HTMLElement>(); const reactButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>(); const clipButton = shallowRef<HTMLElement>();
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); const appearNote = computed(() => getAppearNote(note.value));
const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>(); const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
const isMyRenote = $i && ($i.id === note.value.userId); const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(false); const showContent = ref(false);

View File

@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<MkInfo v-if="user.username.includes('.')">{{ i18n.ts.isSystemAccount }}</MkInfo> <MkInfo v-if="['instance.actor', 'relay.actor'].includes(user.username)">{{ i18n.ts.isSystemAccount }}</MkInfo>
<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink> <FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink>

View File

@ -21,12 +21,12 @@ SPDX-License-Identifier: AGPL-3.0-only
].includes(log.type), ].includes(log.type),
[$style.logYellow]: [ [$style.logYellow]: [
'markSensitiveDriveFile', 'markSensitiveDriveFile',
'resetPassword' 'resetPassword',
'suspendRemoteInstance',
].includes(log.type), ].includes(log.type),
[$style.logRed]: [ [$style.logRed]: [
'suspend', 'suspend',
'deleteRole', 'deleteRole',
'suspendRemoteInstance',
'deleteGlobalAnnouncement', 'deleteGlobalAnnouncement',
'deleteUserAnnouncement', 'deleteUserAnnouncement',
'deleteCustomEmoji', 'deleteCustomEmoji',
@ -36,6 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
'deleteAvatarDecoration', 'deleteAvatarDecoration',
'deleteSystemWebhook', 'deleteSystemWebhook',
'deleteAbuseReportNotificationRecipient', 'deleteAbuseReportNotificationRecipient',
'deleteAccount',
].includes(log.type) ].includes(log.type)
}" }"
>{{ i18n.ts._moderationLogTypes[log.type] }}</b> >{{ i18n.ts._moderationLogTypes[log.type] }}</b>
@ -72,6 +73,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="log.type === 'createAbuseReportNotificationRecipient'">: {{ log.info.recipient.name }}</span> <span v-else-if="log.type === 'createAbuseReportNotificationRecipient'">: {{ log.info.recipient.name }}</span>
<span v-else-if="log.type === 'updateAbuseReportNotificationRecipient'">: {{ log.info.before.name }}</span> <span v-else-if="log.type === 'updateAbuseReportNotificationRecipient'">: {{ log.info.before.name }}</span>
<span v-else-if="log.type === 'deleteAbuseReportNotificationRecipient'">: {{ log.info.recipient.name }}</span> <span v-else-if="log.type === 'deleteAbuseReportNotificationRecipient'">: {{ log.info.recipient.name }}</span>
<span v-else-if="log.type === 'deleteAccount'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
</template> </template>
<template #icon> <template #icon>
<MkAvatar :user="log.user" :class="$style.avatar"/> <MkAvatar :user="log.user" :class="$style.avatar"/>
@ -143,7 +145,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</template> </template>
<template v-else-if="log.type === 'updateRemoteInstanceNote'"> <template v-else-if="log.type === 'updateRemoteInstanceNote'">
<div>{{ i18n.ts.user }}: {{ log.info.userId }}</div>
<div :class="$style.diff"> <div :class="$style.diff">
<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/> <CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
</div> </div>

View File

@ -33,11 +33,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSelect> </MkSelect>
</div> </div>
<div :class="$style.inputs"> <div :class="$style.inputs">
<MkInput v-model="searchUsername" style="flex: 1;" type="text" :spellcheck="false" @update:modelValue="$refs.users.reload()"> <MkInput v-model="searchUsername" style="flex: 1;" type="text" :spellcheck="false">
<template #prefix>@</template> <template #prefix>@</template>
<template #label>{{ i18n.ts.username }}</template> <template #label>{{ i18n.ts.username }}</template>
</MkInput> </MkInput>
<MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()"> <MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'">
<template #prefix>@</template> <template #prefix>@</template>
<template #label>{{ i18n.ts.host }}</template> <template #label>{{ i18n.ts.host }}</template>
</MkInput> </MkInput>

View File

@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { i18n } from '@/i18n.js';
export function getPageBlockList() {
return [
{ value: 'section', text: i18n.ts._pages.blocks.section },
{ value: 'text', text: i18n.ts._pages.blocks.text },
{ value: 'image', text: i18n.ts._pages.blocks.image },
{ value: 'note', text: i18n.ts._pages.blocks.note },
];
}

View File

@ -29,6 +29,7 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { deepClone } from '@/scripts/clone.js'; import { deepClone } from '@/scripts/clone.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { getPageBlockList } from '@/pages/page-editor/common.js';
const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue')); const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue'));
@ -53,11 +54,9 @@ watch(children, () => {
deep: true, deep: true,
}); });
const getPageBlockList = inject<(any) => any>('getPageBlockList');
async function rename() { async function rename() {
const { canceled, result: title } = await os.inputText({ const { canceled, result: title } = await os.inputText({
title: 'Enter title', title: i18n.ts._pages.enterSectionTitle,
default: props.modelValue.title, default: props.modelValue.title,
}); });
if (canceled) return; if (canceled) return;

View File

@ -77,6 +77,7 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { mainRouter } from '@/router/main.js'; import { mainRouter } from '@/router/main.js';
import { getPageBlockList } from '@/pages/page-editor/common.js';
const props = defineProps<{ const props = defineProps<{
initPageId?: string; initPageId?: string;
@ -101,7 +102,6 @@ const alignCenter = ref(false);
const hideTitleWhenPinned = ref(false); const hideTitleWhenPinned = ref(false);
provide('readonly', readonly.value); provide('readonly', readonly.value);
provide('getPageBlockList', getPageBlockList);
watch(eyeCatchingImageId, async () => { watch(eyeCatchingImageId, async () => {
if (eyeCatchingImageId.value == null) { if (eyeCatchingImageId.value == null) {
@ -216,15 +216,6 @@ async function add() {
content.value.push({ id, type }); content.value.push({ id, type });
} }
function getPageBlockList() {
return [
{ value: 'section', text: i18n.ts._pages.blocks.section },
{ value: 'text', text: i18n.ts._pages.blocks.text },
{ value: 'image', text: i18n.ts._pages.blocks.image },
{ value: 'note', text: i18n.ts._pages.blocks.note },
];
}
function setEyeCatchingImage(img) { function setEyeCatchingImage(img) {
selectFile(img.currentTarget ?? img.target, null).then(file => { selectFile(img.currentTarget ?? img.target, null).then(file => {
eyeCatchingImageId.value = file.id; eyeCatchingImageId.value = file.id;

View File

@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, watch, provide, shallowRef, ref } from 'vue'; import { computed, watch, provide, shallowRef, ref, onMounted, onActivated } from 'vue';
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue'; import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
import MkTimeline from '@/components/MkTimeline.vue'; import MkTimeline from '@/components/MkTimeline.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
@ -53,15 +53,18 @@ import { deepMerge } from '@/scripts/merge.js';
import { MenuItem } from '@/types/menu.js'; import { MenuItem } from '@/types/menu.js';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';
import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js'; import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
import type { BasicTimelineType } from '@/timelines.js';
provide('shouldOmitHeaderTitle', true); provide('shouldOmitHeaderTitle', true);
const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>(); const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>();
const rootEl = shallowRef<HTMLElement>(); const rootEl = shallowRef<HTMLElement>();
type TimelinePageSrc = BasicTimelineType | `list:${string}`;
const queue = ref(0); const queue = ref(0);
const srcWhenNotSignin = ref<'local' | 'global'>(isAvailableBasicTimeline('local') ? 'local' : 'global'); const srcWhenNotSignin = ref<'local' | 'global'>(isAvailableBasicTimeline('local') ? 'local' : 'global');
const src = computed<'home' | 'local' | 'social' | 'global' | `list:${string}`>({ const src = computed<TimelinePageSrc>({
get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value), get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value),
set: (x) => saveSrc(x), set: (x) => saveSrc(x),
}); });
@ -195,7 +198,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
os.popupMenu(items, ev.currentTarget ?? ev.target); os.popupMenu(items, ev.currentTarget ?? ev.target);
} }
function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | `list:${string}`): void { function saveSrc(newSrc: TimelinePageSrc): void {
const out = deepMerge({ src: newSrc }, defaultStore.state.tl); const out = deepMerge({ src: newSrc }, defaultStore.state.tl);
if (newSrc.startsWith('userList:')) { if (newSrc.startsWith('userList:')) {
@ -236,6 +239,19 @@ function closeTutorial(): void {
defaultStore.set('timelineTutorials', before); defaultStore.set('timelineTutorials', before);
} }
function switchTlIfNeeded() {
if (isBasicTimeline(src.value) && !availableBasicTimelines().includes(src.value)) {
src.value = availableBasicTimelines()[0];
}
}
onMounted(() => {
switchTlIfNeeded();
});
onActivated(() => {
switchTlIfNeeded();
});
const headerActions = computed(() => { const headerActions = computed(() => {
const tmp = [ const tmp = [
{ {

View File

@ -0,0 +1,10 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as Misskey from 'misskey-js';
export function getAppearNote(note: Misskey.entities.Note) {
return Misskey.note.isPureRenote(note) ? note.renote : note;
}

View File

@ -20,6 +20,7 @@ import { clipsCache, favoritedChannelsCache } from '@/cache.js';
import { MenuItem } from '@/types/menu.js'; import { MenuItem } from '@/types/menu.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { isSupportShare } from '@/scripts/navigator.js'; import { isSupportShare } from '@/scripts/navigator.js';
import { getAppearNote } from '@/scripts/get-appear-note.js';
export async function getNoteClipMenu(props: { export async function getNoteClipMenu(props: {
note: Misskey.entities.Note; note: Misskey.entities.Note;
@ -34,14 +35,7 @@ export async function getNoteClipMenu(props: {
} }
} }
const isRenote = ( const appearNote = getAppearNote(props.note);
props.note.renote != null &&
props.note.text == null &&
props.note.fileIds.length === 0 &&
props.note.poll == null
);
const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
const clips = await clipsCache.fetch(); const clips = await clipsCache.fetch();
const menu: MenuItem[] = [...clips.map(clip => ({ const menu: MenuItem[] = [...clips.map(clip => ({
@ -164,14 +158,7 @@ export function getNoteMenu(props: {
isDeleted: Ref<boolean>; isDeleted: Ref<boolean>;
currentClip?: Misskey.entities.Clip; currentClip?: Misskey.entities.Clip;
}) { }) {
const isRenote = ( const appearNote = getAppearNote(props.note);
props.note.renote != null &&
props.note.text == null &&
props.note.fileIds.length === 0 &&
props.note.poll == null
);
const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
const cleanups = [] as (() => void)[]; const cleanups = [] as (() => void)[];
@ -248,6 +235,7 @@ export function getNoteMenu(props: {
} }
async function unclip(): Promise<void> { async function unclip(): Promise<void> {
if (!props.currentClip) return;
os.apiWithDialog('clips/remove-note', { clipId: props.currentClip.id, noteId: appearNote.id }); os.apiWithDialog('clips/remove-note', { clipId: props.currentClip.id, noteId: appearNote.id });
props.isDeleted.value = true; props.isDeleted.value = true;
} }
@ -267,8 +255,8 @@ export function getNoteMenu(props: {
function share(): void { function share(): void {
navigator.share({ navigator.share({
title: i18n.tsx.noteOf({ user: appearNote.user.name }), title: i18n.tsx.noteOf({ user: appearNote.user.name ?? appearNote.user.username }),
text: appearNote.text, text: appearNote.text ?? '',
url: `${url}/notes/${appearNote.id}`, url: `${url}/notes/${appearNote.id}`,
}); });
} }
@ -509,14 +497,7 @@ export function getRenoteMenu(props: {
renoteButton: ShallowRef<HTMLElement | undefined>; renoteButton: ShallowRef<HTMLElement | undefined>;
mock?: boolean; mock?: boolean;
}) { }) {
const isRenote = ( const appearNote = getAppearNote(props.note);
props.note.renote != null &&
props.note.text == null &&
props.note.fileIds.length === 0 &&
props.note.poll == null
);
const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
const channelRenoteItems: MenuItem[] = []; const channelRenoteItems: MenuItem[] = [];
const normalRenoteItems: MenuItem[] = []; const normalRenoteItems: MenuItem[] = [];

View File

@ -1172,6 +1172,7 @@ declare namespace entities {
export { export {
ID, ID,
DateString, DateString,
PureRenote,
PageEvent, PageEvent,
ModerationLog, ModerationLog,
ServerStats, ServerStats,
@ -2277,6 +2278,9 @@ type ISigninHistoryRequest = operations['i___signin-history']['requestBody']['co
// @public (undocumented) // @public (undocumented)
type ISigninHistoryResponse = operations['i___signin-history']['responses']['200']['content']['application/json']; type ISigninHistoryResponse = operations['i___signin-history']['responses']['200']['content']['application/json'];
// @public (undocumented)
function isPureRenote(note: Note): note is PureRenote;
// @public (undocumented) // @public (undocumented)
type IUnpinRequest = operations['i___unpin']['requestBody']['content']['application/json']; type IUnpinRequest = operations['i___unpin']['requestBody']['content']['application/json'];
@ -2478,10 +2482,13 @@ type ModerationLog = {
} | { } | {
type: 'deleteAbuseReportNotificationRecipient'; type: 'deleteAbuseReportNotificationRecipient';
info: ModerationLogPayloads['deleteAbuseReportNotificationRecipient']; info: ModerationLogPayloads['deleteAbuseReportNotificationRecipient'];
} | {
type: 'deleteAccount';
info: ModerationLogPayloads['deleteAccount'];
}); });
// @public (undocumented) // @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", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient"]; 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", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount"];
// @public (undocumented) // @public (undocumented)
type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json']; type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];
@ -2510,6 +2517,13 @@ type MyAppsResponse = operations['my___apps']['responses']['200']['content']['ap
// @public (undocumented) // @public (undocumented)
type Note = components['schemas']['Note']; type Note = components['schemas']['Note'];
declare namespace note {
export {
isPureRenote
}
}
export { note }
// @public (undocumented) // @public (undocumented)
type NoteFavorite = components['schemas']['NoteFavorite']; type NoteFavorite = components['schemas']['NoteFavorite'];
@ -2750,6 +2764,15 @@ type PinnedUsersResponse = operations['pinned-users']['responses']['200']['conte
// @public (undocumented) // @public (undocumented)
type PromoReadRequest = operations['promo___read']['requestBody']['content']['application/json']; type PromoReadRequest = operations['promo___read']['requestBody']['content']['application/json'];
// Warning: (ae-forgotten-export) The symbol "AllNullRecord" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "NonNullableRecord" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
type PureRenote = Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'> & AllNullRecord<Pick<Note, 'reply' | 'replyId' | 'text' | 'cw' | 'poll'>> & {
files: [];
fileIds: [];
} & NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
// @public (undocumented) // @public (undocumented)
type QueueCount = components['schemas']['QueueCount']; type QueueCount = components['schemas']['QueueCount'];
@ -2829,6 +2852,9 @@ type ReversiShowGameResponse = operations['reversi___show-game']['responses']['2
// @public (undocumented) // @public (undocumented)
type ReversiSurrenderRequest = operations['reversi___surrender']['requestBody']['content']['application/json']; type ReversiSurrenderRequest = operations['reversi___surrender']['requestBody']['content']['application/json'];
// @public (undocumented)
export const reversiUpdateKeys: ["map", "bw", "isLlotheo", "canPutEverywhere", "loopedBoard", "timeLimitForEachTurn"];
// @public (undocumented) // @public (undocumented)
type ReversiVerifyRequest = operations['reversi___verify']['requestBody']['content']['application/json']; type ReversiVerifyRequest = operations['reversi___verify']['requestBody']['content']['application/json'];
@ -3226,7 +3252,7 @@ type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody']['
// Warnings were encountered during analysis: // Warnings were encountered during analysis:
// //
// src/entities.ts:35:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/entities.ts:49:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:220:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:220:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:230:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:230:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts

View File

@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"name": "misskey-js", "name": "misskey-js",
"version": "2024.8.0-alpha.0", "version": "2024.8.0-alpha.1",
"description": "Misskey SDK for JavaScript", "description": "Misskey SDK for JavaScript",
"license": "MIT", "license": "MIT",
"main": "./built/index.js", "main": "./built/index.js",

View File

@ -1,11 +1,16 @@
import type { operations } from './autogen/types.js'; import type { operations } from './autogen/types.js';
import type { import type {
AbuseReportNotificationRecipient, Ad, AbuseReportNotificationRecipient,
Ad,
Announcement, Announcement,
EmojiDetailed, InviteCode, EmojiDetailed,
InviteCode,
MetaDetailed, MetaDetailed,
Note, Note,
Role, SystemWebhook, UserLite, Role,
ReversiGameDetailed,
SystemWebhook,
UserLite,
} from './autogen/models.js'; } from './autogen/models.js';
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned'] as const; export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned'] as const;
@ -149,6 +154,7 @@ export const moderationLogTypes = [
'createAbuseReportNotificationRecipient', 'createAbuseReportNotificationRecipient',
'updateAbuseReportNotificationRecipient', 'updateAbuseReportNotificationRecipient',
'deleteAbuseReportNotificationRecipient', 'deleteAbuseReportNotificationRecipient',
'deleteAccount',
] as const; ] as const;
// See: packages/backend/src/core/ReversiService.ts@L410 // See: packages/backend/src/core/ReversiService.ts@L410
@ -159,7 +165,7 @@ export const reversiUpdateKeys = [
'canPutEverywhere', 'canPutEverywhere',
'loopedBoard', 'loopedBoard',
'timeLimitForEachTurn', 'timeLimitForEachTurn',
] as const; ] as const satisfies (keyof ReversiGameDetailed)[];
export type ReversiUpdateKey = typeof reversiUpdateKeys[number]; export type ReversiUpdateKey = typeof reversiUpdateKeys[number];
@ -387,4 +393,9 @@ export type ModerationLogPayloads = {
recipientId: string; recipientId: string;
recipient: AbuseReportNotificationRecipient; recipient: AbuseReportNotificationRecipient;
}; };
deleteAccount: {
userId: string;
userUsername: string;
userHost: string | null;
};
}; };

View File

@ -3,6 +3,7 @@ import {
Announcement, Announcement,
EmojiDetailed, EmojiDetailed,
MeDetailed, MeDetailed,
Note,
Page, Page,
Role, Role,
RolePolicies, RolePolicies,
@ -16,6 +17,19 @@ export * from './autogen/models.js';
export type ID = string; export type ID = string;
export type DateString = string; export type DateString = string;
type NonNullableRecord<T> = {
[P in keyof T]-?: NonNullable<T[P]>;
};
type AllNullRecord<T> = {
[P in keyof T]: null;
};
export type PureRenote =
Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'>
& AllNullRecord<Pick<Note, 'reply' | 'replyId' | 'text' | 'cw' | 'poll'>>
& { files: []; fileIds: []; }
& NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
export type PageEvent = { export type PageEvent = {
pageId: Page['id']; pageId: Page['id'];
event: string; event: string;
@ -159,6 +173,9 @@ export type ModerationLog = {
} | { } | {
type: 'deleteAbuseReportNotificationRecipient'; type: 'deleteAbuseReportNotificationRecipient';
info: ModerationLogPayloads['deleteAbuseReportNotificationRecipient']; info: ModerationLogPayloads['deleteAbuseReportNotificationRecipient'];
} | {
type: 'deleteAccount';
info: ModerationLogPayloads['deleteAccount'];
}); });
export type ServerStats = { export type ServerStats = {

View File

@ -22,6 +22,7 @@ export const mutedNoteReasons = consts.mutedNoteReasons;
export const followingVisibilities = consts.followingVisibilities; export const followingVisibilities = consts.followingVisibilities;
export const followersVisibilities = consts.followersVisibilities; export const followersVisibilities = consts.followersVisibilities;
export const moderationLogTypes = consts.moderationLogTypes; export const moderationLogTypes = consts.moderationLogTypes;
export const reversiUpdateKeys = consts.reversiUpdateKeys;
// api extractor not supported yet // api extractor not supported yet
//export * as api from './api.js'; //export * as api from './api.js';
@ -29,4 +30,5 @@ export const moderationLogTypes = consts.moderationLogTypes;
import * as api from './api.js'; import * as api from './api.js';
import * as entities from './entities.js'; import * as entities from './entities.js';
import * as acct from './acct.js'; import * as acct from './acct.js';
export { api, entities, acct }; import * as note from './note.js';
export { api, entities, acct, note };

View File

@ -0,0 +1,12 @@
import type { Note, PureRenote } from './entities.js';
export function isPureRenote(note: Note): note is PureRenote {
return (
note.renote != null &&
note.reply == null &&
note.text == null &&
note.cw == null &&
(note.fileIds == null || note.fileIds.length === 0) &&
note.poll == null
);
}

View File

@ -51,8 +51,8 @@ importers:
version: 4.4.0(encoding@0.1.13) version: 4.4.0(encoding@0.1.13)
devDependencies: devDependencies:
'@misskey-dev/eslint-plugin': '@misskey-dev/eslint-plugin':
specifier: 2.0.2 specifier: 2.0.3
version: 2.0.2(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0))(eslint@9.8.0)(globals@15.8.0) version: 2.0.3(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0))(eslint@9.8.0)(globals@15.8.0)
'@types/node': '@types/node':
specifier: 20.14.12 specifier: 20.14.12
version: 20.14.12 version: 20.14.12
@ -3235,8 +3235,8 @@ packages:
'@misskey-dev/browser-image-resizer@2024.1.0': '@misskey-dev/browser-image-resizer@2024.1.0':
resolution: {integrity: sha512-4EnO0zLW5NDtng3Gaz5MuT761uiuoOuplwX18wBqgj8w56LTU5BjLn/vbHwDIIe0j2gwqDYhMb7bDjmr1/Fomg==} resolution: {integrity: sha512-4EnO0zLW5NDtng3Gaz5MuT761uiuoOuplwX18wBqgj8w56LTU5BjLn/vbHwDIIe0j2gwqDYhMb7bDjmr1/Fomg==}
'@misskey-dev/eslint-plugin@2.0.2': '@misskey-dev/eslint-plugin@2.0.3':
resolution: {integrity: sha512-bnTqxCSP0CIN0xSpIGib13bz+K8/3e4h8OlQjuCPlhZF7oFwtn339EZM8yJkHg6gdfciV8KOr3gzlLyG3jiVEQ==} resolution: {integrity: sha512-Gd7chezh53Gq4BZ+Tw/uRi4t5DocXR+vTFuTSRpwrZjbyTk6tWMHLV0EtaGY/6GT+Q6WH3UMJYExsFyHFK+JPw==}
peerDependencies: peerDependencies:
'@eslint/compat': '>= 1' '@eslint/compat': '>= 1'
'@typescript-eslint/eslint-plugin': '>= 7' '@typescript-eslint/eslint-plugin': '>= 7'
@ -14472,7 +14472,7 @@ snapshots:
'@misskey-dev/browser-image-resizer@2024.1.0': {} '@misskey-dev/browser-image-resizer@2024.1.0': {}
'@misskey-dev/eslint-plugin@2.0.2(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0))(eslint@9.8.0)(globals@15.8.0)': '@misskey-dev/eslint-plugin@2.0.3(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0))(eslint@9.8.0)(globals@15.8.0)':
dependencies: dependencies:
'@eslint/compat': 1.1.1 '@eslint/compat': 1.1.1
'@typescript-eslint/eslint-plugin': 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4) '@typescript-eslint/eslint-plugin': 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4)