> :
+ never :
+ never :
+ any;
type ObjectSchemaType = NullOrUndefined
>;
@@ -227,30 +246,30 @@ export type SchemaTypeDef
=
p['type'] extends 'number' ? number :
p['type'] extends 'string' ? (
p['enum'] extends readonly (string | null)[] ?
- p['enum'][number] :
- p['format'] extends 'date-time' ? string : // Dateにする??
- string
+ p['enum'][number] :
+ p['format'] extends 'date-time' ? string : // Dateにする??
+ string
) :
- p['type'] extends 'boolean' ? boolean :
- p['type'] extends 'object' ? ObjectSchemaTypeDef
:
- p['type'] extends 'array' ? (
- p['items'] extends OfSchema ? (
- p['items']['anyOf'] extends ReadonlyArray ? UnionSchemaType>[] :
- p['items']['oneOf'] extends ReadonlyArray ? ArrayUnion>> :
- p['items']['allOf'] extends ReadonlyArray ? UnionToIntersection>>[] :
- never
+ p['type'] extends 'boolean' ? boolean :
+ p['type'] extends 'object' ? ObjectSchemaTypeDef :
+ p['type'] extends 'array' ? (
+ p['items'] extends OfSchema ? (
+ p['items']['anyOf'] extends ReadonlyArray ? UnionSchemaType>[] :
+ p['items']['oneOf'] extends ReadonlyArray ? ArrayUnion>> :
+ p['items']['allOf'] extends ReadonlyArray ? UnionToIntersection>>[] :
+ never
+ ) :
+ p['prefixItems'] extends ReadonlyArray ? (
+ p['items'] extends NonNullable ? [...ArrayToTuple, ...SchemaType
[]] :
+ p['items'] extends false ? ArrayToTuple
:
+ p['unevaluatedItems'] extends false ? ArrayToTuple
:
+ [...ArrayToTuple
, ...unknown[]]
+ ) :
+ p['items'] extends NonNullable ? SchemaType[] :
+ any[]
) :
- p['prefixItems'] extends ReadonlyArray ? (
- p['items'] extends NonNullable ? [...ArrayToTuple, ...SchemaType
[]] :
- p['items'] extends false ? ArrayToTuple
:
- p['unevaluatedItems'] extends false ? ArrayToTuple
:
- [...ArrayToTuple
, ...unknown[]]
- ) :
- p['items'] extends NonNullable ? SchemaType[] :
- any[]
- ) :
- p['anyOf'] extends ReadonlyArray ? UnionSchemaType & PartialIntersection> :
- p['oneOf'] extends ReadonlyArray ? UnionSchemaType :
- any;
+ p['anyOf'] extends ReadonlyArray ? UnionSchemaType & PartialIntersection> :
+ p['oneOf'] extends ReadonlyArray ? UnionSchemaType :
+ any;
export type SchemaType
= NullOrUndefined
>;
diff --git a/packages/backend/src/models/Antenna.ts b/packages/backend/src/models/Antenna.ts
index 33e6f48189..17ec0c0f79 100644
--- a/packages/backend/src/models/Antenna.ts
+++ b/packages/backend/src/models/Antenna.ts
@@ -100,4 +100,9 @@ export class MiAntenna {
default: false,
})
public localOnly: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public excludeNotesInSensitiveChannel: boolean;
}
diff --git a/packages/backend/src/models/ChatApproval.ts b/packages/backend/src/models/ChatApproval.ts
new file mode 100644
index 0000000000..55c9f07e9a
--- /dev/null
+++ b/packages/backend/src/models/ChatApproval.ts
@@ -0,0 +1,39 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { id } from './util/id.js';
+import { MiUser } from './User.js';
+
+@Entity('chat_approval')
+@Index(['userId', 'otherId'], { unique: true })
+export class MiChatApproval {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column({
+ ...id(),
+ })
+ public userId: MiUser['id'];
+
+ @ManyToOne(type => MiUser, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public user: MiUser | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ })
+ public otherId: MiUser['id'];
+
+ @ManyToOne(type => MiUser, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public other: MiUser | null;
+}
diff --git a/packages/backend/src/models/ChatMessage.ts b/packages/backend/src/models/ChatMessage.ts
new file mode 100644
index 0000000000..3d2b64268e
--- /dev/null
+++ b/packages/backend/src/models/ChatMessage.ts
@@ -0,0 +1,85 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { id } from './util/id.js';
+import { MiUser } from './User.js';
+import { MiDriveFile } from './DriveFile.js';
+import { MiChatRoom } from './ChatRoom.js';
+
+@Entity('chat_message')
+export class MiChatMessage {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column({
+ ...id(),
+ })
+ public fromUserId: MiUser['id'];
+
+ @ManyToOne(type => MiUser, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public fromUser: MiUser | null;
+
+ @Index()
+ @Column({
+ ...id(), nullable: true,
+ })
+ public toUserId: MiUser['id'] | null;
+
+ @ManyToOne(type => MiUser, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public toUser: MiUser | null;
+
+ @Index()
+ @Column({
+ ...id(), nullable: true,
+ })
+ public toRoomId: MiChatRoom['id'] | null;
+
+ @ManyToOne(type => MiChatRoom, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public toRoom: MiChatRoom | null;
+
+ @Column('varchar', {
+ length: 4096, nullable: true,
+ })
+ public text: string | null;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ })
+ public uri: string | null;
+
+ @Column({
+ ...id(),
+ array: true, default: '{}',
+ })
+ public reads: MiUser['id'][];
+
+ @Column({
+ ...id(),
+ nullable: true,
+ })
+ public fileId: MiDriveFile['id'] | null;
+
+ @ManyToOne(type => MiDriveFile, {
+ onDelete: 'SET NULL',
+ })
+ @JoinColumn()
+ public file: MiDriveFile | null;
+
+ @Column('varchar', {
+ length: 1024, array: true, default: '{}',
+ })
+ public reactions: string[];
+}
diff --git a/packages/backend/src/models/ChatRoom.ts b/packages/backend/src/models/ChatRoom.ts
new file mode 100644
index 0000000000..ad2a910b78
--- /dev/null
+++ b/packages/backend/src/models/ChatRoom.ts
@@ -0,0 +1,41 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { id } from './util/id.js';
+import { MiUser } from './User.js';
+
+@Entity('chat_room')
+export class MiChatRoom {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('varchar', {
+ length: 256,
+ })
+ public name: string;
+
+ @Index()
+ @Column({
+ ...id(),
+ })
+ public ownerId: MiUser['id'];
+
+ @ManyToOne(type => MiUser, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public owner: MiUser | null;
+
+ @Column('varchar', {
+ length: 2048, default: '',
+ })
+ public description: string;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public isArchived: boolean;
+}
diff --git a/packages/backend/src/models/ChatRoomInvitation.ts b/packages/backend/src/models/ChatRoomInvitation.ts
new file mode 100644
index 0000000000..36ce12bc92
--- /dev/null
+++ b/packages/backend/src/models/ChatRoomInvitation.ts
@@ -0,0 +1,45 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { id } from './util/id.js';
+import { MiUser } from './User.js';
+import { MiChatRoom } from './ChatRoom.js';
+
+@Entity('chat_room_invitation')
+@Index(['userId', 'roomId'], { unique: true })
+export class MiChatRoomInvitation {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column({
+ ...id(),
+ })
+ public userId: MiUser['id'];
+
+ @ManyToOne(type => MiUser, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public user: MiUser | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ })
+ public roomId: MiChatRoom['id'];
+
+ @ManyToOne(type => MiChatRoom, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public room: MiChatRoom | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public ignored: boolean;
+}
diff --git a/packages/backend/src/models/ChatRoomMembership.ts b/packages/backend/src/models/ChatRoomMembership.ts
new file mode 100644
index 0000000000..3cb5524859
--- /dev/null
+++ b/packages/backend/src/models/ChatRoomMembership.ts
@@ -0,0 +1,45 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { id } from './util/id.js';
+import { MiUser } from './User.js';
+import { MiChatRoom } from './ChatRoom.js';
+
+@Entity('chat_room_membership')
+@Index(['userId', 'roomId'], { unique: true })
+export class MiChatRoomMembership {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column({
+ ...id(),
+ })
+ public userId: MiUser['id'];
+
+ @ManyToOne(type => MiUser, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public user: MiUser | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ })
+ public roomId: MiChatRoom['id'];
+
+ @ManyToOne(type => MiChatRoom, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public room: MiChatRoom | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public isMuted: boolean;
+}
diff --git a/packages/backend/src/models/NoteUnread.ts b/packages/backend/src/models/NoteUnread.ts
deleted file mode 100644
index c759181117..0000000000
--- a/packages/backend/src/models/NoteUnread.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { id } from './util/id.js';
-import { MiUser } from './User.js';
-import { MiNote } from './Note.js';
-import type { MiChannel } from './Channel.js';
-
-@Entity('note_unread')
-@Index(['userId', 'noteId'], { unique: true })
-export class MiNoteUnread {
- @PrimaryColumn(id())
- public id: string;
-
- @Index()
- @Column(id())
- public userId: MiUser['id'];
-
- @ManyToOne(type => MiUser, {
- onDelete: 'CASCADE',
- })
- @JoinColumn()
- public user: MiUser | null;
-
- @Index()
- @Column(id())
- public noteId: MiNote['id'];
-
- @ManyToOne(type => MiNote, {
- onDelete: 'CASCADE',
- })
- @JoinColumn()
- public note: MiNote | null;
-
- /**
- * メンションか否か
- */
- @Index()
- @Column('boolean')
- public isMentioned: boolean;
-
- /**
- * ダイレクト投稿か否か
- */
- @Index()
- @Column('boolean')
- public isSpecified: boolean;
-
- //#region Denormalized fields
- @Index()
- @Column({
- ...id(),
- comment: '[Denormalized]',
- })
- public noteUserId: MiUser['id'];
-
- @Index()
- @Column({
- ...id(),
- nullable: true,
- comment: '[Denormalized]',
- })
- public noteChannelId: MiChannel['id'] | null;
- //#endregion
-}
diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts
index 5772ace338..5764a307b0 100644
--- a/packages/backend/src/models/Notification.ts
+++ b/packages/backend/src/models/Notification.ts
@@ -75,6 +75,12 @@ export type MiNotification = {
id: string;
createdAt: string;
roleId: MiRole['id'];
+} | {
+ type: 'chatRoomInvitationReceived';
+ id: string;
+ createdAt: string;
+ notifierId: MiUser['id'];
+ invitationId: string;
} | {
type: 'achievementEarned';
id: string;
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index 6d6fcce601..f0da414aae 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -43,7 +43,6 @@ import {
MiNoteFavorite,
MiNoteReaction,
MiNoteThreadMuting,
- MiNoteUnread,
MiPage,
MiPageLike,
MiPasswordResetRequest,
@@ -79,6 +78,11 @@ import {
MiUserPublickey,
MiUserSecurityKey,
MiWebhook,
+ MiChatMessage,
+ MiChatRoom,
+ MiChatRoomMembership,
+ MiChatRoomInvitation,
+ MiChatApproval,
} from './_.js';
import type { Provider } from '@nestjs/common';
import type { DataSource } from 'typeorm';
@@ -137,12 +141,6 @@ const $noteReactionsRepository: Provider = {
inject: [DI.db],
};
-const $noteUnreadsRepository: Provider = {
- provide: DI.noteUnreadsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiNoteUnread).extend(miRepository as MiRepository),
- inject: [DI.db],
-};
-
const $pollsRepository: Provider = {
provide: DI.pollsRepository,
useFactory: (db: DataSource) => db.getRepository(MiPoll).extend(miRepository as MiRepository),
@@ -289,7 +287,7 @@ const $swSubscriptionsRepository: Provider = {
const $systemAccountsRepository: Provider = {
provide: DI.systemAccountsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiSystemAccount),
+ useFactory: (db: DataSource) => db.getRepository(MiSystemAccount).extend(miRepository as MiRepository),
inject: [DI.db],
};
@@ -307,7 +305,7 @@ const $abuseUserReportsRepository: Provider = {
const $abuseReportNotificationRecipientRepository: Provider = {
provide: DI.abuseReportNotificationRecipientRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient),
+ useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient).extend(miRepository as MiRepository),
inject: [DI.db],
};
@@ -445,7 +443,7 @@ const $webhooksRepository: Provider = {
const $systemWebhooksRepository: Provider = {
provide: DI.systemWebhooksRepository,
- useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook),
+ useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook).extend(miRepository as MiRepository),
inject: [DI.db],
};
@@ -497,6 +495,36 @@ const $userMemosRepository: Provider = {
inject: [DI.db],
};
+const $chatMessagesRepository: Provider = {
+ provide: DI.chatMessagesRepository,
+ useFactory: (db: DataSource) => db.getRepository(MiChatMessage).extend(miRepository as MiRepository),
+ inject: [DI.db],
+};
+
+const $chatRoomsRepository: Provider = {
+ provide: DI.chatRoomsRepository,
+ useFactory: (db: DataSource) => db.getRepository(MiChatRoom).extend(miRepository as MiRepository),
+ inject: [DI.db],
+};
+
+const $chatRoomMembershipsRepository: Provider = {
+ provide: DI.chatRoomMembershipsRepository,
+ useFactory: (db: DataSource) => db.getRepository(MiChatRoomMembership).extend(miRepository as MiRepository),
+ inject: [DI.db],
+};
+
+const $chatRoomInvitationsRepository: Provider = {
+ provide: DI.chatRoomInvitationsRepository,
+ useFactory: (db: DataSource) => db.getRepository(MiChatRoomInvitation).extend(miRepository as MiRepository),
+ inject: [DI.db],
+};
+
+const $chatApprovalsRepository: Provider = {
+ provide: DI.chatApprovalsRepository,
+ useFactory: (db: DataSource) => db.getRepository(MiChatApproval).extend(miRepository as MiRepository),
+ inject: [DI.db],
+};
+
const $bubbleGameRecordsRepository: Provider = {
provide: DI.bubbleGameRecordsRepository,
useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository),
@@ -521,7 +549,6 @@ const $reversiGamesRepository: Provider = {
$noteFavoritesRepository,
$noteThreadMutingsRepository,
$noteReactionsRepository,
- $noteUnreadsRepository,
$pollsRepository,
$pollVotesRepository,
$userProfilesRepository,
@@ -581,6 +608,11 @@ const $reversiGamesRepository: Provider = {
$flashsRepository,
$flashLikesRepository,
$userMemosRepository,
+ $chatMessagesRepository,
+ $chatRoomsRepository,
+ $chatRoomMembershipsRepository,
+ $chatRoomInvitationsRepository,
+ $chatApprovalsRepository,
$bubbleGameRecordsRepository,
$reversiGamesRepository,
],
@@ -594,7 +626,6 @@ const $reversiGamesRepository: Provider = {
$noteFavoritesRepository,
$noteThreadMutingsRepository,
$noteReactionsRepository,
- $noteUnreadsRepository,
$pollsRepository,
$pollVotesRepository,
$userProfilesRepository,
@@ -654,6 +685,11 @@ const $reversiGamesRepository: Provider = {
$flashsRepository,
$flashLikesRepository,
$userMemosRepository,
+ $chatMessagesRepository,
+ $chatRoomsRepository,
+ $chatRoomMembershipsRepository,
+ $chatRoomInvitationsRepository,
+ $chatApprovalsRepository,
$bubbleGameRecordsRepository,
$reversiGamesRepository,
],
diff --git a/packages/backend/src/models/Role.ts b/packages/backend/src/models/Role.ts
index a173971b2c..4c7da252bd 100644
--- a/packages/backend/src/models/Role.ts
+++ b/packages/backend/src/models/Role.ts
@@ -248,6 +248,11 @@ export class MiRole {
})
public isExplorable: boolean;
+ @Column('boolean', {
+ default: false,
+ })
+ public preserveAssignmentOnMoveAccount: boolean;
+
@Column('boolean', {
default: false,
})
diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts
index 630240efde..bc652cea62 100644
--- a/packages/backend/src/models/User.ts
+++ b/packages/backend/src/models/User.ts
@@ -225,6 +225,17 @@ export class MiUser {
})
public emojis: string[];
+ // チャットを許可する相手
+ // everyone: 誰からでも
+ // followers: フォロワーのみ
+ // following: フォローしているユーザーのみ
+ // mutual: 相互フォローのみ
+ // none: 誰からも受け付けない
+ @Column('varchar', {
+ length: 128, default: 'mutual',
+ })
+ public chatScope: 'everyone' | 'followers' | 'following' | 'mutual' | 'none';
+
@Index()
@Column('varchar', {
length: 128, nullable: true,
diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts
index 6791363c07..6d9a1f5ad1 100644
--- a/packages/backend/src/models/_.ts
+++ b/packages/backend/src/models/_.ts
@@ -41,7 +41,6 @@ import { MiNote } from '@/models/Note.js';
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
import { MiNoteReaction } from '@/models/NoteReaction.js';
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
-import { MiNoteUnread } from '@/models/NoteUnread.js';
import { MiPage } from '@/models/Page.js';
import { MiPageLike } from '@/models/PageLike.js';
import { MiPasswordResetRequest } from '@/models/PasswordResetRequest.js';
@@ -76,6 +75,11 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js';
import { MiFlash } from '@/models/Flash.js';
import { MiFlashLike } from '@/models/FlashLike.js';
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
+import { MiChatMessage } from '@/models/ChatMessage.js';
+import { MiChatRoom } from '@/models/ChatRoom.js';
+import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js';
+import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js';
+import { MiChatApproval } from '@/models/ChatApproval.js';
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
import { MiReversiGame } from '@/models/ReversiGame.js';
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
@@ -158,7 +162,6 @@ export {
MiNoteFavorite,
MiNoteReaction,
MiNoteThreadMuting,
- MiNoteUnread,
MiPage,
MiPageLike,
MiPasswordResetRequest,
@@ -193,6 +196,11 @@ export {
MiFlash,
MiFlashLike,
MiUserMemo,
+ MiChatMessage,
+ MiChatRoom,
+ MiChatRoomMembership,
+ MiChatRoomInvitation,
+ MiChatApproval,
MiBubbleGameRecord,
MiReversiGame,
};
@@ -231,7 +239,6 @@ export type NotesRepository = Repository & MiRepository;
export type NoteFavoritesRepository = Repository & MiRepository;
export type NoteReactionsRepository = Repository & MiRepository;
export type NoteThreadMutingsRepository = Repository & MiRepository;
-export type NoteUnreadsRepository = Repository & MiRepository;
export type PagesRepository = Repository & MiRepository;
export type PageLikesRepository = Repository & MiRepository;
export type PasswordResetRequestsRepository = Repository & MiRepository;
@@ -266,5 +273,10 @@ export type RoleAssignmentsRepository = Repository & MiReposit
export type FlashsRepository = Repository & MiRepository;
export type FlashLikesRepository = Repository & MiRepository;
export type UserMemoRepository = Repository & MiRepository;
+export type ChatMessagesRepository = Repository & MiRepository;
+export type ChatRoomsRepository = Repository & MiRepository;
+export type ChatRoomMembershipsRepository = Repository & MiRepository;
+export type ChatRoomInvitationsRepository = Repository & MiRepository;
+export type ChatApprovalsRepository = Repository & MiRepository;
export type BubbleGameRecordsRepository = Repository & MiRepository;
export type ReversiGamesRepository = Repository & MiRepository;
diff --git a/packages/backend/src/models/json-schema/antenna.ts b/packages/backend/src/models/json-schema/antenna.ts
index b5b9a5b42c..eca7563066 100644
--- a/packages/backend/src/models/json-schema/antenna.ts
+++ b/packages/backend/src/models/json-schema/antenna.ts
@@ -100,5 +100,10 @@ export const packedAntennaSchema = {
optional: false, nullable: false,
default: false,
},
+ excludeNotesInSensitiveChannel: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ default: false,
+ },
},
} as const;
diff --git a/packages/backend/src/models/json-schema/chat-message.ts b/packages/backend/src/models/json-schema/chat-message.ts
new file mode 100644
index 0000000000..3b5e85ab69
--- /dev/null
+++ b/packages/backend/src/models/json-schema/chat-message.ts
@@ -0,0 +1,256 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const packedChatMessageSchema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ createdAt: {
+ type: 'string',
+ format: 'date-time',
+ optional: false, nullable: false,
+ },
+ fromUserId: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ fromUser: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'UserLite',
+ },
+ toUserId: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
+ toUser: {
+ type: 'object',
+ optional: true, nullable: true,
+ ref: 'UserLite',
+ },
+ toRoomId: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
+ toRoom: {
+ type: 'object',
+ optional: true, nullable: true,
+ ref: 'ChatRoom',
+ },
+ text: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
+ fileId: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
+ file: {
+ type: 'object',
+ optional: true, nullable: true,
+ ref: 'DriveFile',
+ },
+ isRead: {
+ type: 'boolean',
+ optional: true, nullable: false,
+ },
+ reactions: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ reaction: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ user: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'UserLite',
+ },
+ },
+ },
+ },
+ },
+} as const;
+
+export const packedChatMessageLiteSchema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ createdAt: {
+ type: 'string',
+ format: 'date-time',
+ optional: false, nullable: false,
+ },
+ fromUserId: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ fromUser: {
+ type: 'object',
+ optional: true, nullable: false,
+ ref: 'UserLite',
+ },
+ toUserId: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
+ toRoomId: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
+ text: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
+ fileId: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
+ file: {
+ type: 'object',
+ optional: true, nullable: true,
+ ref: 'DriveFile',
+ },
+ reactions: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ reaction: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ user: {
+ type: 'object',
+ optional: true, nullable: true,
+ ref: 'UserLite',
+ },
+ },
+ },
+ },
+ },
+} as const;
+
+export const packedChatMessageLiteFor1on1Schema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ createdAt: {
+ type: 'string',
+ format: 'date-time',
+ optional: false, nullable: false,
+ },
+ fromUserId: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ toUserId: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ text: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
+ fileId: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
+ file: {
+ type: 'object',
+ optional: true, nullable: true,
+ ref: 'DriveFile',
+ },
+ reactions: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ reaction: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
+ },
+ },
+ },
+} as const;
+
+export const packedChatMessageLiteForRoomSchema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ createdAt: {
+ type: 'string',
+ format: 'date-time',
+ optional: false, nullable: false,
+ },
+ fromUserId: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ fromUser: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'UserLite',
+ },
+ toRoomId: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ text: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
+ fileId: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
+ file: {
+ type: 'object',
+ optional: true, nullable: true,
+ ref: 'DriveFile',
+ },
+ reactions: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ reaction: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ user: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'UserLite',
+ },
+ },
+ },
+ },
+ },
+} as const;
diff --git a/packages/backend/src/models/json-schema/chat-room-invitation.ts b/packages/backend/src/models/json-schema/chat-room-invitation.ts
new file mode 100644
index 0000000000..204c959b2c
--- /dev/null
+++ b/packages/backend/src/models/json-schema/chat-room-invitation.ts
@@ -0,0 +1,37 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const packedChatRoomInvitationSchema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ createdAt: {
+ type: 'string',
+ format: 'date-time',
+ optional: false, nullable: false,
+ },
+ userId: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ user: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'UserLite',
+ },
+ roomId: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ room: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'ChatRoom',
+ },
+ },
+} as const;
diff --git a/packages/backend/src/models/json-schema/chat-room-membership.ts b/packages/backend/src/models/json-schema/chat-room-membership.ts
new file mode 100644
index 0000000000..adb73f9dde
--- /dev/null
+++ b/packages/backend/src/models/json-schema/chat-room-membership.ts
@@ -0,0 +1,37 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const packedChatRoomMembershipSchema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ createdAt: {
+ type: 'string',
+ format: 'date-time',
+ optional: false, nullable: false,
+ },
+ userId: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ user: {
+ type: 'object',
+ optional: true, nullable: false,
+ ref: 'UserLite',
+ },
+ roomId: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ room: {
+ type: 'object',
+ optional: true, nullable: false,
+ ref: 'ChatRoom',
+ },
+ },
+} as const;
diff --git a/packages/backend/src/models/json-schema/chat-room.ts b/packages/backend/src/models/json-schema/chat-room.ts
new file mode 100644
index 0000000000..e97556e378
--- /dev/null
+++ b/packages/backend/src/models/json-schema/chat-room.ts
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const packedChatRoomSchema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ createdAt: {
+ type: 'string',
+ format: 'date-time',
+ optional: false, nullable: false,
+ },
+ ownerId: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ owner: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'UserLite',
+ },
+ name: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ description: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ isMuted: {
+ type: 'boolean',
+ optional: true, nullable: false,
+ },
+ },
+} as const;
diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts
index 1e25c355ca..2cd7620af0 100644
--- a/packages/backend/src/models/json-schema/meta.ts
+++ b/packages/backend/src/models/json-schema/meta.ts
@@ -211,6 +211,38 @@ export const packedMetaLiteSchema = {
type: 'boolean',
optional: false, nullable: false,
},
+ sentryForFrontend: {
+ type: 'object',
+ optional: false, nullable: true,
+ properties: {
+ options: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ dsn: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
+ additionalProperties: true,
+ },
+ vueIntegration: {
+ type: 'object',
+ optional: true, nullable: true,
+ additionalProperties: true,
+ },
+ browserTracingIntegration: {
+ type: 'object',
+ optional: true, nullable: true,
+ additionalProperties: true,
+ },
+ replayIntegration: {
+ type: 'object',
+ optional: true, nullable: true,
+ additionalProperties: true,
+ },
+ },
+ },
mediaProxy: {
type: 'string',
optional: false, nullable: false,
diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts
index 1638b2b3c7..7f23d2d6a1 100644
--- a/packages/backend/src/models/json-schema/notification.ts
+++ b/packages/backend/src/models/json-schema/notification.ts
@@ -287,6 +287,21 @@ export const packedNotificationSchema = {
optional: false, nullable: false,
},
},
+ }, {
+ type: 'object',
+ properties: {
+ ...baseSchema.properties,
+ type: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: ['chatRoomInvitationReceived'],
+ },
+ invitation: {
+ type: 'object',
+ ref: 'ChatRoomInvitation',
+ optional: false, nullable: false,
+ },
+ },
}, {
type: 'object',
properties: {
diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts
index 3537de94c8..1cfcb830e0 100644
--- a/packages/backend/src/models/json-schema/role.ts
+++ b/packages/backend/src/models/json-schema/role.ts
@@ -292,6 +292,11 @@ export const packedRolePoliciesSchema = {
type: 'boolean',
optional: false, nullable: false,
},
+ chatAvailability: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: ['available', 'readonly', 'unavailable'],
+ },
},
} as const;
@@ -385,6 +390,11 @@ export const packedRoleSchema = {
optional: false, nullable: false,
example: false,
},
+ preserveAssignmentOnMoveAccount: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ example: false,
+ },
canEditMembersByModerator: {
type: 'boolean',
optional: false, nullable: false,
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index 38631f907d..e475296702 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -358,6 +358,15 @@ export const packedUserDetailedNotMeOnlySchema = {
nullable: false, optional: false,
enum: ['public', 'followers', 'private'],
},
+ chatScope: {
+ type: 'string',
+ nullable: false, optional: false,
+ enum: ['everyone', 'following', 'followers', 'mutual', 'none'],
+ },
+ canChat: {
+ type: 'boolean',
+ nullable: false, optional: false,
+ },
roles: {
type: 'array',
nullable: false, optional: false,
@@ -540,6 +549,10 @@ export const packedMeDetailedOnlySchema = {
type: 'boolean',
nullable: false, optional: false,
},
+ hasUnreadChatMessages: {
+ type: 'boolean',
+ nullable: false, optional: false,
+ },
hasUnreadNotification: {
type: 'boolean',
nullable: false, optional: false,
@@ -599,6 +612,7 @@ export const packedMeDetailedOnlySchema = {
receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
followRequestAccepted: { optional: true, ...notificationRecieveConfig },
roleAssigned: { optional: true, ...notificationRecieveConfig },
+ chatRoomInvitationReceived: { optional: true, ...notificationRecieveConfig },
achievementEarned: { optional: true, ...notificationRecieveConfig },
app: { optional: true, ...notificationRecieveConfig },
test: { optional: true, ...notificationRecieveConfig },
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index c2c1ef6a31..e0c314dc27 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -8,6 +8,9 @@ import pg from 'pg';
import { DataSource, Logger } from 'typeorm';
import * as highlight from 'cli-highlight';
import { entities as charts } from '@/core/chart/entities.js';
+import { Config } from '@/config.js';
+import MisskeyLogger from '@/logger.js';
+import { bindThis } from '@/decorators.js';
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
@@ -43,7 +46,6 @@ import { MiNote } from '@/models/Note.js';
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
import { MiNoteReaction } from '@/models/NoteReaction.js';
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
-import { MiNoteUnread } from '@/models/NoteUnread.js';
import { MiPage } from '@/models/Page.js';
import { MiPageLike } from '@/models/PageLike.js';
import { MiPasswordResetRequest } from '@/models/PasswordResetRequest.js';
@@ -77,13 +79,14 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js';
import { MiFlash } from '@/models/Flash.js';
import { MiFlashLike } from '@/models/FlashLike.js';
import { MiUserMemo } from '@/models/UserMemo.js';
+import { MiChatMessage } from '@/models/ChatMessage.js';
+import { MiChatRoom } from '@/models/ChatRoom.js';
+import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js';
+import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js';
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
import { MiReversiGame } from '@/models/ReversiGame.js';
-
-import { Config } from '@/config.js';
-import MisskeyLogger from '@/logger.js';
-import { bindThis } from '@/decorators.js';
-import { MiSystemAccount } from './models/SystemAccount.js';
+import { MiChatApproval } from '@/models/ChatApproval.js';
+import { MiSystemAccount } from '@/models/SystemAccount.js';
pg.types.setTypeParser(20, Number);
@@ -196,7 +199,6 @@ export const entities = [
MiNoteFavorite,
MiNoteReaction,
MiNoteThreadMuting,
- MiNoteUnread,
MiPage,
MiPageLike,
MiGalleryPost,
@@ -238,6 +240,11 @@ export const entities = [
MiFlash,
MiFlashLike,
MiUserMemo,
+ MiChatMessage,
+ MiChatRoom,
+ MiChatRoomMembership,
+ MiChatRoomInvitation,
+ MiChatApproval,
MiBubbleGameRecord,
MiReversiGame,
...charts,
diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts
index 3ab0b815f2..0223650329 100644
--- a/packages/backend/src/server/ServerModule.ts
+++ b/packages/backend/src/server/ServerModule.ts
@@ -44,6 +44,8 @@ import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
import { UserListChannelService } from './api/stream/channels/user-list.js';
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
+import { ChatUserChannelService } from './api/stream/channels/chat-user.js';
+import { ChatRoomChannelService } from './api/stream/channels/chat-room.js';
import { ReversiChannelService } from './api/stream/channels/reversi.js';
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
@@ -84,6 +86,8 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
GlobalTimelineChannelService,
HashtagChannelService,
RoleTimelineChannelService,
+ ChatUserChannelService,
+ ChatRoomChannelService,
ReversiChannelService,
ReversiGameChannelService,
HomeTimelineChannelService,
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 9399aa61b0..a42fdaf730 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -391,10 +391,10 @@ export class ApiCallService implements OnApplicationShutdown {
}
}
- if (ep.meta.requireRolePolicy != null && (this.meta.rootUserId !== user!.id)) {
+ if (ep.meta.requiredRolePolicy != null && (this.meta.rootUserId !== user!.id)) {
const myRoles = await this.roleService.getUserRoles(user!.id);
const policies = await this.roleService.getUserPolicies(user!.id);
- if (!policies[ep.meta.requireRolePolicy] && !myRoles.some(r => r.isAdministrator)) {
+ if (!policies[ep.meta.requiredRolePolicy] && !myRoles.some(r => r.isAdministrator)) {
throw new ApiError({
message: 'You are not assigned to a required role.',
code: 'ROLE_PERMISSION_DENIED',
diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts
index 429bc9a179..21f2f0b7e2 100644
--- a/packages/backend/src/server/api/StreamingApiServerService.ts
+++ b/packages/backend/src/server/api/StreamingApiServerService.ts
@@ -9,7 +9,6 @@ import * as Redis from 'ioredis';
import * as WebSocket from 'ws';
import { DI } from '@/di-symbols.js';
import type { UsersRepository, MiAccessToken } from '@/models/_.js';
-import { NoteReadService } from '@/core/NoteReadService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
@@ -36,7 +35,6 @@ export class StreamingApiServerService {
private usersRepository: UsersRepository,
private cacheService: CacheService,
- private noteReadService: NoteReadService,
private authenticateService: AuthenticateService,
private channelsService: ChannelsService,
private notificationService: NotificationService,
@@ -98,7 +96,6 @@ export class StreamingApiServerService {
const stream = new MainStreamConnection(
this.channelsService,
- this.noteReadService,
this.notificationService,
this.cacheService,
this.channelFollowingService,
diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts
index c85e910d71..2a48117fe6 100644
--- a/packages/backend/src/server/api/endpoint-list.ts
+++ b/packages/backend/src/server/api/endpoint-list.ts
@@ -266,7 +266,6 @@ export * as 'i/notifications-grouped' from './endpoints/i/notifications-grouped.
export * as 'i/page-likes' from './endpoints/i/page-likes.js';
export * as 'i/pages' from './endpoints/i/pages.js';
export * as 'i/pin' from './endpoints/i/pin.js';
-export * as 'i/read-all-unread-notes' from './endpoints/i/read-all-unread-notes.js';
export * as 'i/read-announcement' from './endpoints/i/read-announcement.js';
export * as 'i/regenerate-token' from './endpoints/i/regenerate-token.js';
export * as 'i/registry/get' from './endpoints/i/registry/get.js';
@@ -400,4 +399,28 @@ export * as 'users/search' from './endpoints/users/search.js';
export * as 'users/search-by-username-and-host' from './endpoints/users/search-by-username-and-host.js';
export * as 'users/show' from './endpoints/users/show.js';
export * as 'users/update-memo' from './endpoints/users/update-memo.js';
+export * as 'chat/messages/create-to-user' from './endpoints/chat/messages/create-to-user.js';
+export * as 'chat/messages/create-to-room' from './endpoints/chat/messages/create-to-room.js';
+export * as 'chat/messages/delete' from './endpoints/chat/messages/delete.js';
+export * as 'chat/messages/show' from './endpoints/chat/messages/show.js';
+export * as 'chat/messages/react' from './endpoints/chat/messages/react.js';
+export * as 'chat/messages/unreact' from './endpoints/chat/messages/unreact.js';
+export * as 'chat/messages/user-timeline' from './endpoints/chat/messages/user-timeline.js';
+export * as 'chat/messages/room-timeline' from './endpoints/chat/messages/room-timeline.js';
+export * as 'chat/messages/search' from './endpoints/chat/messages/search.js';
+export * as 'chat/rooms/create' from './endpoints/chat/rooms/create.js';
+export * as 'chat/rooms/delete' from './endpoints/chat/rooms/delete.js';
+export * as 'chat/rooms/join' from './endpoints/chat/rooms/join.js';
+export * as 'chat/rooms/leave' from './endpoints/chat/rooms/leave.js';
+export * as 'chat/rooms/mute' from './endpoints/chat/rooms/mute.js';
+export * as 'chat/rooms/show' from './endpoints/chat/rooms/show.js';
+export * as 'chat/rooms/owned' from './endpoints/chat/rooms/owned.js';
+export * as 'chat/rooms/joining' from './endpoints/chat/rooms/joining.js';
+export * as 'chat/rooms/update' from './endpoints/chat/rooms/update.js';
+export * as 'chat/rooms/members' from './endpoints/chat/rooms/members.js';
+export * as 'chat/rooms/invitations/create' from './endpoints/chat/rooms/invitations/create.js';
+export * as 'chat/rooms/invitations/ignore' from './endpoints/chat/rooms/invitations/ignore.js';
+export * as 'chat/rooms/invitations/inbox' from './endpoints/chat/rooms/invitations/inbox.js';
+export * as 'chat/rooms/invitations/outbox' from './endpoints/chat/rooms/invitations/outbox.js';
+export * as 'chat/history' from './endpoints/chat/history.js';
export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js';
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 7f4ca9c0e0..03c729ed18 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -39,7 +39,7 @@ interface IEndpointMetaBase {
*/
readonly requireAdmin?: boolean;
- readonly requireRolePolicy?: KeyOf<'RolePolicies'>;
+ readonly requiredRolePolicy?: KeyOf<'RolePolicies'>;
/**
* 引っ越し済みのユーザーによるリクエストを禁止するか
diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
index 87d80cbe80..0121c302ac 100644
--- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
@@ -12,7 +12,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireRolePolicy: 'canManageAvatarDecorations',
+ requiredRolePolicy: 'canManageAvatarDecorations',
kind: 'write:admin:avatar-decorations',
res: {
diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts
index 3a5673d99d..13660d0b8c 100644
--- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts
+++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts
@@ -13,7 +13,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireRolePolicy: 'canManageAvatarDecorations',
+ requiredRolePolicy: 'canManageAvatarDecorations',
kind: 'write:admin:avatar-decorations',
errors: {
},
diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
index d785f085ac..d4d9a7235b 100644
--- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
@@ -13,7 +13,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireRolePolicy: 'canManageAvatarDecorations',
+ requiredRolePolicy: 'canManageAvatarDecorations',
kind: 'read:admin:avatar-decorations',
res: {
diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts
index 34b3b5a11f..22476a6888 100644
--- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts
@@ -13,7 +13,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireRolePolicy: 'canManageAvatarDecorations',
+ requiredRolePolicy: 'canManageAvatarDecorations',
kind: 'write:admin:avatar-decorations',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts
index a30a080e59..1459351d37 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts
@@ -11,7 +11,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireRolePolicy: 'canManageCustomEmojis',
+ requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
index 53256565f6..3852146177 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
@@ -16,7 +16,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireRolePolicy: 'canManageCustomEmojis',
+ requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
index 87b58ff6f6..cf03859ce5 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
@@ -17,7 +17,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireRolePolicy: 'canManageCustomEmojis',
+ requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts
index cec9f700c3..7993edcc07 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts
@@ -11,7 +11,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireRolePolicy: 'canManageCustomEmojis',
+ requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts
index 50c45b6ac5..87ed3f5f18 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts
@@ -11,7 +11,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireRolePolicy: 'canManageCustomEmojis',
+ requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts
index 8e5f69c894..7ca931eb21 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts
@@ -10,7 +10,7 @@ import { QueueService } from '@/core/QueueService.js';
export const meta = {
secure: true,
requireCredential: true,
- requireRolePolicy: 'canManageCustomEmojis',
+ requiredRolePolicy: 'canManageCustomEmojis',
} as const;
export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
index 0889ceb76f..b44007962d 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
@@ -16,7 +16,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireRolePolicy: 'canManageCustomEmojis',
+ requiredRolePolicy: 'canManageCustomEmojis',
kind: 'read:admin:emoji',
res: {
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
index ffb5dbf4b5..4342e178cc 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
@@ -16,7 +16,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireRolePolicy: 'canManageCustomEmojis',
+ requiredRolePolicy: 'canManageCustomEmojis',
kind: 'read:admin:emoji',
res: {
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts
index 0fa119eabe..161c3b9f37 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts
@@ -11,7 +11,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireRolePolicy: 'canManageCustomEmojis',
+ requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts
index d9ee18699c..2e700809d8 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts
@@ -11,7 +11,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireRolePolicy: 'canManageCustomEmojis',
+ requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts
index dc25df2767..ee87858b0e 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts
@@ -11,7 +11,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireRolePolicy: 'canManageCustomEmojis',
+ requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts
index 4ba99faab7..7ab5916951 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts
@@ -11,7 +11,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireRolePolicy: 'canManageCustomEmojis',
+ requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
index e3aaa051c1..6834a6d213 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -14,7 +14,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireRolePolicy: 'canManageCustomEmojis',
+ requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/create.ts b/packages/backend/src/server/api/endpoints/admin/roles/create.ts
index e0c02f7a5d..f92f7ebaeb 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/create.ts
@@ -36,6 +36,7 @@ export const paramDef = {
isAdministrator: { type: 'boolean' },
isExplorable: { type: 'boolean', default: false }, // optional for backward compatibility
asBadge: { type: 'boolean' },
+ preserveAssignmentOnMoveAccount: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' },
displayOrder: { type: 'number' },
policies: {
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts
index 465ad7aaaf..175adcb63f 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts
@@ -41,6 +41,7 @@ export const paramDef = {
isAdministrator: { type: 'boolean' },
isExplorable: { type: 'boolean' },
asBadge: { type: 'boolean' },
+ preserveAssignmentOnMoveAccount: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' },
displayOrder: { type: 'number' },
policies: {
@@ -78,6 +79,7 @@ export default class extends Endpoint { // eslint-
isAdministrator: ps.isAdministrator,
isExplorable: ps.isExplorable,
asBadge: ps.asBadge,
+ preserveAssignmentOnMoveAccount: ps.preserveAssignmentOnMoveAccount,
canEditMembersByModerator: ps.canEditMembersByModerator,
displayOrder: ps.displayOrder,
policies: ps.policies,
diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts
index 655bd32bce..1ba6853dbe 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts
@@ -106,6 +106,7 @@ export const meta = {
receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
followRequestAccepted: { optional: true, ...notificationRecieveConfig },
roleAssigned: { optional: true, ...notificationRecieveConfig },
+ chatRoomInvitationReceived: { optional: true, ...notificationRecieveConfig },
achievementEarned: { optional: true, ...notificationRecieveConfig },
app: { optional: true, ...notificationRecieveConfig },
test: { optional: true, ...notificationRecieveConfig },
diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts
index e0c8ddcc84..c075608491 100644
--- a/packages/backend/src/server/api/endpoints/antennas/create.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/create.ts
@@ -73,6 +73,7 @@ export const paramDef = {
excludeBots: { type: 'boolean' },
withReplies: { type: 'boolean' },
withFile: { type: 'boolean' },
+ excludeNotesInSensitiveChannel: { type: 'boolean' },
},
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'],
} as const;
@@ -133,6 +134,7 @@ export default class extends Endpoint { // eslint-
excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
+ excludeNotesInSensitiveChannel: ps.excludeNotesInSensitiveChannel,
});
this.globalEventService.publishInternalEvent('antennaCreated', antenna);
diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts
index 49f1df9692..bcec01476f 100644
--- a/packages/backend/src/server/api/endpoints/antennas/notes.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts
@@ -9,7 +9,6 @@ import { Brackets } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { NotesRepository, AntennasRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
-import { NoteReadService } from '@/core/NoteReadService.js';
import { DI } from '@/di-symbols.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { IdService } from '@/core/IdService.js';
@@ -61,9 +60,6 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.redisForTimelines)
- private redisForTimelines: Redis.Redis,
-
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -73,7 +69,6 @@ export default class extends Endpoint { // eslint-
private idService: IdService,
private noteEntityService: NoteEntityService,
private queryService: QueryService,
- private noteReadService: NoteReadService,
private fanoutTimelineService: FanoutTimelineService,
private globalEventService: GlobalEventService,
private channelMutingService: ChannelMutingService,
@@ -131,9 +126,12 @@ export default class extends Endpoint { // eslint-
}));
}
+ // NOTE: センシティブ除外の設定はこのエンドポイントでは無視する。
+ // https://github.com/misskey-dev/misskey/pull/15346#discussion_r1929950255
+
this.queryService.generateVisibilityQuery(query, me);
- this.queryService.generateMutedUserQuery(query, me);
- this.queryService.generateBlockedUserQuery(query, me);
+ this.queryService.generateMutedUserQueryForNotes(query, me);
+ this.queryService.generateBlockedUserQueryForNotes(query, me);
const notes = await query.getMany();
if (sinceId != null && untilId == null) {
@@ -142,8 +140,6 @@ export default class extends Endpoint { // eslint-
notes.sort((a, b) => a.id > b.id ? -1 : 1);
}
- this.noteReadService.read(me.id, notes);
-
return await this.noteEntityService.packMany(notes, me);
});
}
diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts
index 10f26b1912..53fc4db1b7 100644
--- a/packages/backend/src/server/api/endpoints/antennas/update.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/update.ts
@@ -72,6 +72,7 @@ export const paramDef = {
excludeBots: { type: 'boolean' },
withReplies: { type: 'boolean' },
withFile: { type: 'boolean' },
+ excludeNotesInSensitiveChannel: { type: 'boolean' },
},
required: ['antennaId'],
} as const;
@@ -129,6 +130,7 @@ export default class extends Endpoint { // eslint-
excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
+ excludeNotesInSensitiveChannel: ps.excludeNotesInSensitiveChannel,
isActive: true,
lastUsedAt: new Date(),
});
diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts
index b818282341..9fb5393832 100644
--- a/packages/backend/src/server/api/endpoints/channels/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts
@@ -142,8 +142,8 @@ export default class extends Endpoint { // eslint-
query.andWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds });
}
- this.queryService.generateMutedUserQuery(query, me);
- this.queryService.generateBlockedUserQuery(query, me);
+ this.queryService.generateMutedUserQueryForNotes(query, me);
+ this.queryService.generateBlockedUserQueryForNotes(query, me);
}
//#endregion
diff --git a/packages/backend/src/server/api/endpoints/chat/history.ts b/packages/backend/src/server/api/endpoints/chat/history.ts
new file mode 100644
index 0000000000..fdd9055106
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/history.ts
@@ -0,0 +1,75 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'read:chat',
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'ChatMessage',
+ },
+ },
+
+ errors: {
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ room: { type: 'boolean', default: false },
+ },
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatEntityService: ChatEntityService,
+ private chatService: ChatService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
+ const history = ps.room ? await this.chatService.roomHistory(me.id, ps.limit) : await this.chatService.userHistory(me.id, ps.limit);
+
+ const packedMessages = await this.chatEntityService.packMessagesDetailed(history, me);
+
+ if (ps.room) {
+ const roomIds = history.map(m => m.toRoomId!);
+ const readStateMap = await this.chatService.getRoomReadStateMap(me.id, roomIds);
+
+ for (const message of packedMessages) {
+ message.isRead = readStateMap[message.toRoomId!] ?? false;
+ }
+ } else {
+ const otherIds = history.map(m => m.fromUserId === me.id ? m.toUserId! : m.fromUserId!);
+ const readStateMap = await this.chatService.getUserReadStateMap(me.id, otherIds);
+
+ for (const message of packedMessages) {
+ const otherId = message.fromUserId === me.id ? message.toUserId! : message.fromUserId!;
+ message.isRead = readStateMap[otherId] ?? false;
+ }
+ }
+
+ return packedMessages;
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts b/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts
new file mode 100644
index 0000000000..ad2b82e219
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts
@@ -0,0 +1,106 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { GetterService } from '@/server/api/GetterService.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { ChatService } from '@/core/ChatService.js';
+import type { DriveFilesRepository, MiUser } from '@/models/_.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ prohibitMoved: true,
+
+ kind: 'write:chat',
+
+ limit: {
+ duration: ms('1hour'),
+ max: 500,
+ },
+
+ res: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'ChatMessageLiteForRoom',
+ },
+
+ errors: {
+ noSuchRoom: {
+ message: 'No such room.',
+ code: 'NO_SUCH_ROOM',
+ id: '8098520d-2da5-4e8f-8ee1-df78b55a4ec6',
+ },
+
+ noSuchFile: {
+ message: 'No such file.',
+ code: 'NO_SUCH_FILE',
+ id: 'b6accbd3-1d7b-4d9f-bdb7-eb185bac06db',
+ },
+
+ contentRequired: {
+ message: 'Content required. You need to set text or fileId.',
+ code: 'CONTENT_REQUIRED',
+ id: '340517b7-6d04-42c0-bac1-37ee804e3594',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ text: { type: 'string', nullable: true, maxLength: 2000 },
+ fileId: { type: 'string', format: 'misskey:id' },
+ toRoomId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['toRoomId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.driveFilesRepository)
+ private driveFilesRepository: DriveFilesRepository,
+
+ private getterService: GetterService,
+ private chatService: ChatService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
+ const room = await this.chatService.findRoomById(ps.toRoomId);
+ if (room == null) {
+ throw new ApiError(meta.errors.noSuchRoom);
+ }
+
+ let file = null;
+ if (ps.fileId != null) {
+ file = await this.driveFilesRepository.findOneBy({
+ id: ps.fileId,
+ userId: me.id,
+ });
+
+ if (file == null) {
+ throw new ApiError(meta.errors.noSuchFile);
+ }
+ }
+
+ // テキストが無いかつ添付ファイルも無かったらエラー
+ if (ps.text == null && file == null) {
+ throw new ApiError(meta.errors.contentRequired);
+ }
+
+ return await this.chatService.createMessageToRoom(me, room, {
+ text: ps.text,
+ file: file,
+ });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts b/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts
new file mode 100644
index 0000000000..fa34a7d558
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts
@@ -0,0 +1,123 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { GetterService } from '@/server/api/GetterService.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { ChatService } from '@/core/ChatService.js';
+import type { DriveFilesRepository, MiUser } from '@/models/_.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ prohibitMoved: true,
+
+ kind: 'write:chat',
+
+ limit: {
+ duration: ms('1hour'),
+ max: 500,
+ },
+
+ res: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'ChatMessageLiteFor1on1',
+ },
+
+ errors: {
+ recipientIsYourself: {
+ message: 'You can not send a message to yourself.',
+ code: 'RECIPIENT_IS_YOURSELF',
+ id: '17e2ba79-e22a-4cbc-bf91-d327643f4a7e',
+ },
+
+ noSuchUser: {
+ message: 'No such user.',
+ code: 'NO_SUCH_USER',
+ id: '11795c64-40ea-4198-b06e-3c873ed9039d',
+ },
+
+ noSuchFile: {
+ message: 'No such file.',
+ code: 'NO_SUCH_FILE',
+ id: '4372b8e2-185d-4146-8749-2f68864a3e5f',
+ },
+
+ contentRequired: {
+ message: 'Content required. You need to set text or fileId.',
+ code: 'CONTENT_REQUIRED',
+ id: '25587321-b0e6-449c-9239-f8925092942c',
+ },
+
+ youHaveBeenBlocked: {
+ message: 'You cannot send a message because you have been blocked by this user.',
+ code: 'YOU_HAVE_BEEN_BLOCKED',
+ id: 'c15a5199-7422-4968-941a-2a462c478f7d',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ text: { type: 'string', nullable: true, maxLength: 2000 },
+ fileId: { type: 'string', format: 'misskey:id' },
+ toUserId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['toUserId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.driveFilesRepository)
+ private driveFilesRepository: DriveFilesRepository,
+
+ private getterService: GetterService,
+ private chatService: ChatService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
+ let file = null;
+ if (ps.fileId != null) {
+ file = await this.driveFilesRepository.findOneBy({
+ id: ps.fileId,
+ userId: me.id,
+ });
+
+ if (file == null) {
+ throw new ApiError(meta.errors.noSuchFile);
+ }
+ }
+
+ // テキストが無いかつ添付ファイルも無かったらエラー
+ if (ps.text == null && file == null) {
+ throw new ApiError(meta.errors.contentRequired);
+ }
+
+ // Myself
+ if (ps.toUserId === me.id) {
+ throw new ApiError(meta.errors.recipientIsYourself);
+ }
+
+ const toUser = await this.getterService.getUser(ps.toUserId).catch(err => {
+ if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+ throw err;
+ });
+
+ return await this.chatService.createMessageToUser(me, toUser, {
+ text: ps.text,
+ file: file,
+ });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/delete.ts b/packages/backend/src/server/api/endpoints/chat/messages/delete.ts
new file mode 100644
index 0000000000..63b75fb6a7
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/messages/delete.ts
@@ -0,0 +1,54 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'write:chat',
+
+ res: {
+ },
+
+ errors: {
+ noSuchMessage: {
+ message: 'No such message.',
+ code: 'NO_SUCH_MESSAGE',
+ id: '36b67f0e-66a6-414b-83df-992a55294f17',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ messageId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['messageId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatService: ChatService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
+ const message = await this.chatService.findMyMessageById(me.id, ps.messageId);
+ if (message == null) {
+ throw new ApiError(meta.errors.noSuchMessage);
+ }
+ await this.chatService.deleteMessage(message);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/react.ts b/packages/backend/src/server/api/endpoints/chat/messages/react.ts
new file mode 100644
index 0000000000..5f61e7e992
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/messages/react.ts
@@ -0,0 +1,51 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'write:chat',
+
+ res: {
+ },
+
+ errors: {
+ noSuchMessage: {
+ message: 'No such message.',
+ code: 'NO_SUCH_MESSAGE',
+ id: '9b5839b9-0ba0-4351-8c35-37082093d200',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ messageId: { type: 'string', format: 'misskey:id' },
+ reaction: { type: 'string' },
+ },
+ required: ['messageId', 'reaction'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatService: ChatService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
+ await this.chatService.react(ps.messageId, me.id, ps.reaction);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts b/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts
new file mode 100644
index 0000000000..c0e344b889
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts
@@ -0,0 +1,75 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'read:chat',
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'ChatMessageLiteForRoom',
+ },
+ },
+
+ errors: {
+ noSuchRoom: {
+ message: 'No such room.',
+ code: 'NO_SUCH_ROOM',
+ id: 'c4d9f88c-9270-4632-b032-6ed8cee36f7f',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ roomId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['roomId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatEntityService: ChatEntityService,
+ private chatService: ChatService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
+ const room = await this.chatService.findRoomById(ps.roomId);
+ if (room == null) {
+ throw new ApiError(meta.errors.noSuchRoom);
+ }
+
+ if (!await this.chatService.hasPermissionToViewRoomTimeline(me.id, room)) {
+ throw new ApiError(meta.errors.noSuchRoom);
+ }
+
+ const messages = await this.chatService.roomTimeline(room.id, ps.limit, ps.sinceId, ps.untilId);
+
+ this.chatService.readRoomChatMessage(me.id, room.id);
+
+ return await this.chatEntityService.packMessagesLiteForRoom(messages);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/search.ts b/packages/backend/src/server/api/endpoints/chat/messages/search.ts
new file mode 100644
index 0000000000..682597f76d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/messages/search.ts
@@ -0,0 +1,78 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'read:chat',
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'ChatMessage',
+ },
+ },
+
+ errors: {
+ noSuchRoom: {
+ message: 'No such room.',
+ code: 'NO_SUCH_ROOM',
+ id: '460b3669-81b0-4dc9-a997-44442141bf83',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ query: { type: 'string', minLength: 1, maxLength: 256 },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ userId: { type: 'string', format: 'misskey:id', nullable: true },
+ roomId: { type: 'string', format: 'misskey:id', nullable: true },
+ },
+ required: ['query'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatEntityService: ChatEntityService,
+ private chatService: ChatService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
+ if (ps.roomId != null) {
+ const room = await this.chatService.findRoomById(ps.roomId);
+ if (room == null) {
+ throw new ApiError(meta.errors.noSuchRoom);
+ }
+
+ if (!(await this.chatService.isRoomMember(room, me.id))) {
+ throw new ApiError(meta.errors.noSuchRoom);
+ }
+ }
+
+ const messages = await this.chatService.searchMessages(me.id, ps.query, ps.limit, {
+ userId: ps.userId,
+ roomId: ps.roomId,
+ });
+
+ return await this.chatEntityService.packMessagesDetailed(messages, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/show.ts b/packages/backend/src/server/api/endpoints/chat/messages/show.ts
new file mode 100644
index 0000000000..9a2bbb8742
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/messages/show.ts
@@ -0,0 +1,65 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { GetterService } from '@/server/api/GetterService.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
+import { ApiError } from '@/server/api/error.js';
+import { RoleService } from '@/core/RoleService.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'read:chat',
+
+ res: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'ChatMessage',
+ },
+
+ errors: {
+ noSuchMessage: {
+ message: 'No such message.',
+ code: 'NO_SUCH_MESSAGE',
+ id: '3710865b-1848-4da9-8d61-cfed15510b93',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ messageId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['messageId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatService: ChatService,
+ private roleService: RoleService,
+ private chatEntityService: ChatEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
+ const message = await this.chatService.findMessageById(ps.messageId);
+ if (message == null) {
+ throw new ApiError(meta.errors.noSuchMessage);
+ }
+ if (message.fromUserId !== me.id && message.toUserId !== me.id && !(await this.roleService.isModerator(me))) {
+ throw new ApiError(meta.errors.noSuchMessage);
+ }
+ return this.chatEntityService.packMessageDetailed(message, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts b/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts
new file mode 100644
index 0000000000..6784bb6ecf
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts
@@ -0,0 +1,51 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'write:chat',
+
+ res: {
+ },
+
+ errors: {
+ noSuchMessage: {
+ message: 'No such message.',
+ code: 'NO_SUCH_MESSAGE',
+ id: 'c39ea42f-e3ca-428a-ad57-390e0a711595',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ messageId: { type: 'string', format: 'misskey:id' },
+ reaction: { type: 'string' },
+ },
+ required: ['messageId', 'reaction'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatService: ChatService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
+ await this.chatService.unreact(ps.messageId, me.id, ps.reaction);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts b/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts
new file mode 100644
index 0000000000..a057e2e088
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts
@@ -0,0 +1,73 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { GetterService } from '@/server/api/GetterService.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'read:chat',
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'ChatMessageLiteFor1on1',
+ },
+ },
+
+ errors: {
+ noSuchUser: {
+ message: 'No such user.',
+ code: 'NO_SUCH_USER',
+ id: '11795c64-40ea-4198-b06e-3c873ed9039d',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ userId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['userId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatEntityService: ChatEntityService,
+ private chatService: ChatService,
+ private getterService: GetterService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
+ const other = await this.getterService.getUser(ps.userId).catch(err => {
+ if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+ throw err;
+ });
+
+ const messages = await this.chatService.userTimeline(me.id, other.id, ps.limit, ps.sinceId, ps.untilId);
+
+ this.chatService.readUserChatMessage(me.id, other.id);
+
+ return await this.chatEntityService.packMessagesLiteFor1on1(messages);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/create.ts b/packages/backend/src/server/api/endpoints/chat/rooms/create.ts
new file mode 100644
index 0000000000..68a53f0886
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/create.ts
@@ -0,0 +1,63 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ prohibitMoved: true,
+
+ kind: 'write:chat',
+
+ limit: {
+ duration: ms('1day'),
+ max: 10,
+ },
+
+ res: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'ChatRoom',
+ },
+
+ errors: {
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ name: { type: 'string', maxLength: 256 },
+ description: { type: 'string', maxLength: 1024 },
+ },
+ required: ['name'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatService: ChatService,
+ private chatEntityService: ChatEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
+ const room = await this.chatService.createRoom(me, {
+ name: ps.name,
+ description: ps.description ?? '',
+ });
+ return await this.chatEntityService.packRoom(room);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts b/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts
new file mode 100644
index 0000000000..82a8e1f30d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts
@@ -0,0 +1,59 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'write:chat',
+
+ res: {
+ },
+
+ errors: {
+ noSuchRoom: {
+ message: 'No such room.',
+ code: 'NO_SUCH_ROOM',
+ id: 'd4e3753d-97bf-4a19-ab8e-21080fbc0f4b',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roomId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['roomId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatService: ChatService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
+ const room = await this.chatService.findRoomById(ps.roomId);
+ if (room == null) {
+ throw new ApiError(meta.errors.noSuchRoom);
+ }
+
+ if (!await this.chatService.hasPermissionToDeleteRoom(me.id, room)) {
+ throw new ApiError(meta.errors.noSuchRoom);
+ }
+
+ await this.chatService.deleteRoom(room, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/create.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/create.ts
new file mode 100644
index 0000000000..b1f049f2b9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/create.ts
@@ -0,0 +1,69 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ prohibitMoved: true,
+
+ kind: 'write:chat',
+
+ limit: {
+ duration: ms('1day'),
+ max: 50,
+ },
+
+ res: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'ChatRoomInvitation',
+ },
+
+ errors: {
+ noSuchRoom: {
+ message: 'No such room.',
+ code: 'NO_SUCH_ROOM',
+ id: '916f9507-49ba-4e90-b57f-1fd4deaa47a5',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roomId: { type: 'string', format: 'misskey:id' },
+ userId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['roomId', 'userId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatService: ChatService,
+ private chatEntityService: ChatEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
+ const room = await this.chatService.findMyRoomById(me.id, ps.roomId);
+ if (room == null) {
+ throw new ApiError(meta.errors.noSuchRoom);
+ }
+ const invitation = await this.chatService.createRoomInvitation(me.id, room.id, ps.userId);
+ return await this.chatEntityService.packRoomInvitation(invitation, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts
new file mode 100644
index 0000000000..b8a228089b
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts
@@ -0,0 +1,50 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'write:chat',
+
+ res: {
+ },
+
+ errors: {
+ noSuchRoom: {
+ message: 'No such room.',
+ code: 'NO_SUCH_ROOM',
+ id: '5130557e-5a11-4cfb-9cc5-fe60cda5de0d',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roomId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['roomId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatService: ChatService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
+ await this.chatService.ignoreRoomInvitation(me.id, ps.roomId);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts
new file mode 100644
index 0000000000..8a02d1c704
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts
@@ -0,0 +1,56 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'read:chat',
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'ChatRoomInvitation',
+ },
+ },
+
+ errors: {
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ },
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatEntityService: ChatEntityService,
+ private chatService: ChatService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
+ const invitations = await this.chatService.getReceivedRoomInvitationsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId);
+ return this.chatEntityService.packRoomInvitations(invitations, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts
new file mode 100644
index 0000000000..0702ba086c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts
@@ -0,0 +1,69 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'read:chat',
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'ChatRoomInvitation',
+ },
+ },
+
+ errors: {
+ noSuchRoom: {
+ message: 'No such room.',
+ code: 'NO_SUCH_ROOM',
+ id: 'a3c6b309-9717-4316-ae94-a69b53437237',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roomId: { type: 'string', format: 'misskey:id' },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['roomId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatService: ChatService,
+ private chatEntityService: ChatEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
+ const room = await this.chatService.findMyRoomById(me.id, ps.roomId);
+ if (room == null) {
+ throw new ApiError(meta.errors.noSuchRoom);
+ }
+
+ const invitations = await this.chatService.getSentRoomInvitationsWithPagination(ps.roomId, ps.limit, ps.sinceId, ps.untilId);
+ return this.chatEntityService.packRoomInvitations(invitations, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/join.ts b/packages/backend/src/server/api/endpoints/chat/rooms/join.ts
new file mode 100644
index 0000000000..d561f9e03f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/join.ts
@@ -0,0 +1,50 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'write:chat',
+
+ res: {
+ },
+
+ errors: {
+ noSuchRoom: {
+ message: 'No such room.',
+ code: 'NO_SUCH_ROOM',
+ id: '84416476-5ce8-4a2c-b568-9569f1b10733',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roomId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['roomId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatService: ChatService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
+ await this.chatService.joinToRoom(me.id, ps.roomId);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts b/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts
new file mode 100644
index 0000000000..ba9242c762
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts
@@ -0,0 +1,60 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'read:chat',
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'ChatRoomMembership',
+ },
+ },
+
+ errors: {
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ },
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatService: ChatService,
+ private chatEntityService: ChatEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
+ const memberships = await this.chatService.getMyMemberships(me.id, ps.limit, ps.sinceId, ps.untilId);
+
+ return this.chatEntityService.packRoomMemberships(memberships, me, {
+ populateUser: false,
+ populateRoom: true,
+ });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts b/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts
new file mode 100644
index 0000000000..a3ad0c2d6f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts
@@ -0,0 +1,50 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'write:chat',
+
+ res: {
+ },
+
+ errors: {
+ noSuchRoom: {
+ message: 'No such room.',
+ code: 'NO_SUCH_ROOM',
+ id: 'cb7f3179-50e8-4389-8c30-dbe2650a67c9',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roomId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['roomId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatService: ChatService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
+ await this.chatService.leaveRoom(me.id, ps.roomId);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/members.ts b/packages/backend/src/server/api/endpoints/chat/rooms/members.ts
new file mode 100644
index 0000000000..f5ffa21d32
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/members.ts
@@ -0,0 +1,76 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ApiError } from '@/server/api/error.js';
+import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'write:chat',
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'ChatRoomMembership',
+ },
+ },
+
+ errors: {
+ noSuchRoom: {
+ message: 'No such room.',
+ code: 'NO_SUCH_ROOM',
+ id: '7b9fe84c-eafc-4d21-bf89-485458ed2c18',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roomId: { type: 'string', format: 'misskey:id' },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['roomId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatService: ChatService,
+ private chatEntityService: ChatEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
+ const room = await this.chatService.findRoomById(ps.roomId);
+ if (room == null) {
+ throw new ApiError(meta.errors.noSuchRoom);
+ }
+
+ if (!(await this.chatService.isRoomMember(room, me.id))) {
+ throw new ApiError(meta.errors.noSuchRoom);
+ }
+
+ const memberships = await this.chatService.getRoomMembershipsWithPagination(room.id, ps.limit, ps.sinceId, ps.untilId);
+
+ return this.chatEntityService.packRoomMemberships(memberships, me, {
+ populateUser: true,
+ populateRoom: false,
+ });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts b/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts
new file mode 100644
index 0000000000..11cbe7b8b9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts
@@ -0,0 +1,51 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'write:chat',
+
+ res: {
+ },
+
+ errors: {
+ noSuchRoom: {
+ message: 'No such room.',
+ code: 'NO_SUCH_ROOM',
+ id: 'c2cde4eb-8d0f-42f1-8f2f-c4d6bfc8e5df',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roomId: { type: 'string', format: 'misskey:id' },
+ mute: { type: 'boolean' },
+ },
+ required: ['roomId', 'mute'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatService: ChatService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
+ await this.chatService.muteRoom(me.id, ps.roomId, ps.mute);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts b/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts
new file mode 100644
index 0000000000..accf7e1bee
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts
@@ -0,0 +1,56 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'read:chat',
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'ChatRoom',
+ },
+ },
+
+ errors: {
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ },
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatEntityService: ChatEntityService,
+ private chatService: ChatService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
+ const rooms = await this.chatService.getOwnedRoomsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId);
+ return this.chatEntityService.packRooms(rooms, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/show.ts b/packages/backend/src/server/api/endpoints/chat/rooms/show.ts
new file mode 100644
index 0000000000..50da210d81
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/show.ts
@@ -0,0 +1,60 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ApiError } from '@/server/api/error.js';
+import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'read:chat',
+
+ res: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'ChatRoom',
+ },
+
+ errors: {
+ noSuchRoom: {
+ message: 'No such room.',
+ code: 'NO_SUCH_ROOM',
+ id: '857ae02f-8759-4d20-9adb-6e95fffe4fd7',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roomId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['roomId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatService: ChatService,
+ private chatEntityService: ChatEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
+ const room = await this.chatService.findRoomById(ps.roomId);
+ if (room == null) {
+ throw new ApiError(meta.errors.noSuchRoom);
+ }
+
+ return this.chatEntityService.packRoom(room, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/update.ts b/packages/backend/src/server/api/endpoints/chat/rooms/update.ts
new file mode 100644
index 0000000000..0cd62cb040
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/update.ts
@@ -0,0 +1,67 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ApiError } from '@/server/api/error.js';
+import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'write:chat',
+
+ res: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'ChatRoom',
+ },
+
+ errors: {
+ noSuchRoom: {
+ message: 'No such room.',
+ code: 'NO_SUCH_ROOM',
+ id: 'fcdb0f92-bda6-47f9-bd05-343e0e020932',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roomId: { type: 'string', format: 'misskey:id' },
+ name: { type: 'string', maxLength: 256 },
+ description: { type: 'string', maxLength: 1024 },
+ },
+ required: ['roomId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatService: ChatService,
+ private chatEntityService: ChatEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
+ const room = await this.chatService.findMyRoomById(me.id, ps.roomId);
+ if (room == null) {
+ throw new ApiError(meta.errors.noSuchRoom);
+ }
+
+ const updated = await this.chatService.updateRoom(room, {
+ name: ps.name,
+ description: ps.description,
+ });
+
+ return this.chatEntityService.packRoom(updated, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts
index 943c31c894..7638aae442 100644
--- a/packages/backend/src/server/api/endpoints/clips/notes.ts
+++ b/packages/backend/src/server/api/endpoints/clips/notes.ts
@@ -87,8 +87,8 @@ export default class extends Endpoint { // eslint-
if (me) {
this.queryService.generateVisibilityQuery(query, me);
- this.queryService.generateMutedUserQuery(query, me);
- this.queryService.generateBlockedUserQuery(query, me);
+ this.queryService.generateMutedUserQueryForNotes(query, me);
+ this.queryService.generateBlockedUserQueryForNotes(query, me);
}
const notes = await query
diff --git a/packages/backend/src/server/api/endpoints/i/import-antennas.ts b/packages/backend/src/server/api/endpoints/i/import-antennas.ts
index bdf6c065e8..ccec96ffbb 100644
--- a/packages/backend/src/server/api/endpoints/i/import-antennas.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-antennas.ts
@@ -16,7 +16,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
- requireRolePolicy: 'canImportAntennas',
+ requiredRolePolicy: 'canImportAntennas',
prohibitMoved: true,
limit: {
diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts
index d7bb6bcd22..2fa450558b 100644
--- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts
@@ -15,7 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
- requireRolePolicy: 'canImportBlocking',
+ requiredRolePolicy: 'canImportBlocking',
prohibitMoved: true,
limit: {
diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts
index e03192d8c6..9186fca162 100644
--- a/packages/backend/src/server/api/endpoints/i/import-following.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-following.ts
@@ -15,7 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
- requireRolePolicy: 'canImportFollowing',
+ requiredRolePolicy: 'canImportFollowing',
prohibitMoved: true,
limit: {
duration: ms('1hour'),
diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts
index 76b285bb7e..b6dbacd371 100644
--- a/packages/backend/src/server/api/endpoints/i/import-muting.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts
@@ -15,7 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
- requireRolePolicy: 'canImportMuting',
+ requiredRolePolicy: 'canImportMuting',
prohibitMoved: true,
limit: {
diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
index 76ecfd082c..5de0a70bbb 100644
--- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
@@ -15,7 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
- requireRolePolicy: 'canImportUserLists',
+ requiredRolePolicy: 'canImportUserLists',
prohibitMoved: true,
limit: {
duration: ms('1hour'),
diff --git a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts
index dc6ffd3e02..b9c41b057d 100644
--- a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts
+++ b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts
@@ -7,9 +7,13 @@ import { In } from 'typeorm';
import * as Redis from 'ioredis';
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/_.js';
-import { obsoleteNotificationTypes, groupedNotificationTypes, FilterUnionByProperty } from '@/types.js';
+import {
+ obsoleteNotificationTypes,
+ groupedNotificationTypes,
+ FilterUnionByProperty,
+ notificationTypes,
+} from '@/types.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { NoteReadService } from '@/core/NoteReadService.js';
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { DI } from '@/di-symbols.js';
@@ -48,10 +52,10 @@ export const paramDef = {
markAsRead: { type: 'boolean', default: true },
// 後方互換のため、廃止された通知タイプも受け付ける
includeTypes: { type: 'array', items: {
- type: 'string', enum: [...groupedNotificationTypes, ...obsoleteNotificationTypes],
+ type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes],
} },
excludeTypes: { type: 'array', items: {
- type: 'string', enum: [...groupedNotificationTypes, ...obsoleteNotificationTypes],
+ type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes],
} },
},
required: [],
@@ -63,13 +67,9 @@ export default class extends Endpoint { // eslint-
@Inject(DI.redis)
private redisClient: Redis.Redis,
- @Inject(DI.notesRepository)
- private notesRepository: NotesRepository,
-
private idService: IdService,
private notificationEntityService: NotificationEntityService,
private notificationService: NotificationService,
- private noteReadService: NoteReadService,
) {
super(meta, paramDef, async (ps, me) => {
const EXTRA_LIMIT = 100;
@@ -79,31 +79,20 @@ export default class extends Endpoint { // eslint-
return [];
}
// excludeTypes に全指定されている場合はクエリしない
- if (groupedNotificationTypes.every(type => ps.excludeTypes?.includes(type))) {
+ if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) {
return [];
}
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof groupedNotificationTypes[number][];
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof groupedNotificationTypes[number][];
- const limit = (ps.limit + EXTRA_LIMIT) + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
- const notificationsRes = await this.redisClient.xrevrange(
- `notificationTimeline:${me.id}`,
- ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
- ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : '-',
- 'COUNT', limit);
-
- if (notificationsRes.length === 0) {
- return [];
- }
-
- let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as MiNotification[];
-
- if (includeTypes && includeTypes.length > 0) {
- notifications = notifications.filter(notification => includeTypes.includes(notification.type));
- } else if (excludeTypes && excludeTypes.length > 0) {
- notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
- }
+ const notifications = await this.notificationService.getNotifications(me.id, {
+ sinceId: ps.sinceId,
+ untilId: ps.untilId,
+ limit: ps.limit,
+ includeTypes,
+ excludeTypes,
+ });
if (notifications.length === 0) {
return [];
@@ -162,14 +151,6 @@ export default class extends Endpoint { // eslint-
}
groupedNotifications = groupedNotifications.slice(0, ps.limit);
- const noteIds = groupedNotifications
- .filter((notification): notification is FilterUnionByProperty => ['mention', 'reply', 'quote'].includes(notification.type))
- .map(notification => notification.noteId!);
-
- if (noteIds.length > 0) {
- const notes = await this.notesRepository.findBy({ id: In(noteIds) });
- this.noteReadService.read(me.id, notes);
- }
return await this.notificationEntityService.packGroupedMany(groupedNotifications, me.id);
});
diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts
index 2f619380e9..f5a48b2f69 100644
--- a/packages/backend/src/server/api/endpoints/i/notifications.ts
+++ b/packages/backend/src/server/api/endpoints/i/notifications.ts
@@ -9,7 +9,6 @@ import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/_.js';
import { FilterUnionByProperty, notificationTypes, obsoleteNotificationTypes } from '@/types.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { NoteReadService } from '@/core/NoteReadService.js';
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { DI } from '@/di-symbols.js';
@@ -69,7 +68,6 @@ export default class extends Endpoint { // eslint-
private idService: IdService,
private notificationEntityService: NotificationEntityService,
private notificationService: NotificationService,
- private noteReadService: NoteReadService,
) {
super(meta, paramDef, async (ps, me) => {
// includeTypes が空の場合はクエリしない
@@ -84,67 +82,19 @@ export default class extends Endpoint { // eslint-
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
- let sinceTime = ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime().toString() : null;
- let untilTime = ps.untilId ? this.idService.parse(ps.untilId).date.getTime().toString() : null;
-
- let notifications: MiNotification[];
- for (;;) {
- let notificationsRes: [id: string, fields: string[]][];
-
- // sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照
- if (sinceTime && !untilTime) {
- notificationsRes = await this.redisClient.xrange(
- `notificationTimeline:${me.id}`,
- '(' + sinceTime,
- '+',
- 'COUNT', ps.limit);
- } else {
- notificationsRes = await this.redisClient.xrevrange(
- `notificationTimeline:${me.id}`,
- untilTime ? '(' + untilTime : '+',
- sinceTime ? '(' + sinceTime : '-',
- 'COUNT', ps.limit);
- }
-
- if (notificationsRes.length === 0) {
- return [];
- }
-
- notifications = notificationsRes.map(x => JSON.parse(x[1][1])) as MiNotification[];
-
- if (includeTypes && includeTypes.length > 0) {
- notifications = notifications.filter(notification => includeTypes.includes(notification.type));
- } else if (excludeTypes && excludeTypes.length > 0) {
- notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
- }
-
- if (notifications.length !== 0) {
- // 通知が1件以上ある場合は返す
- break;
- }
-
- // フィルタしたことで通知が0件になった場合、次のページを取得する
- if (ps.sinceId && !ps.untilId) {
- sinceTime = notificationsRes[notificationsRes.length - 1][0];
- } else {
- untilTime = notificationsRes[notificationsRes.length - 1][0];
- }
- }
+ const notifications = await this.notificationService.getNotifications(me.id, {
+ sinceId: ps.sinceId,
+ untilId: ps.untilId,
+ limit: ps.limit,
+ includeTypes,
+ excludeTypes,
+ });
// Mark all as read
if (ps.markAsRead) {
this.notificationService.readAllNotification(me.id);
}
- const noteIds = notifications
- .filter((notification): notification is FilterUnionByProperty => ['mention', 'reply', 'quote'].includes(notification.type))
- .map(notification => notification.noteId);
-
- if (noteIds.length > 0) {
- const notes = await this.notesRepository.findBy({ id: In(noteIds) });
- this.noteReadService.read(me.id, notes);
- }
-
return await this.notificationEntityService.packMany(notifications, me.id);
});
}
diff --git a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts
deleted file mode 100644
index d1a8eccb1d..0000000000
--- a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { Inject, Injectable } from '@nestjs/common';
-import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { NoteUnreadsRepository } from '@/models/_.js';
-import { GlobalEventService } from '@/core/GlobalEventService.js';
-import { DI } from '@/di-symbols.js';
-
-export const meta = {
- tags: ['account'],
-
- requireCredential: true,
-
- kind: 'write:account',
-} as const;
-
-export const paramDef = {
- type: 'object',
- properties: {},
- required: [],
-} as const;
-
-@Injectable()
-export default class extends Endpoint { // eslint-disable-line import/no-default-export
- constructor(
- @Inject(DI.noteUnreadsRepository)
- private noteUnreadsRepository: NoteUnreadsRepository,
-
- private globalEventService: GlobalEventService,
- ) {
- super(meta, paramDef, async (ps, me) => {
- // Remove documents
- await this.noteUnreadsRepository.delete({
- userId: me.id,
- });
-
- // 全て既読になったイベントを発行
- this.globalEventService.publishMainStream(me.id, 'readAllUnreadMentions');
- this.globalEventService.publishMainStream(me.id, 'readAllUnreadSpecifiedNotes');
- });
- }
-}
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 4c72879b73..082d97f5d4 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -190,6 +190,7 @@ export const paramDef = {
autoSensitive: { type: 'boolean' },
followingVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
followersVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
+ chatScope: { type: 'string', enum: ['everyone', 'followers', 'following', 'mutual', 'none'] },
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
mutedWords: muteWords,
hardMutedWords: muteWords,
@@ -211,6 +212,7 @@ export const paramDef = {
receiveFollowRequest: notificationRecieveConfig,
followRequestAccepted: notificationRecieveConfig,
roleAssigned: notificationRecieveConfig,
+ chatRoomInvitationReceived: notificationRecieveConfig,
achievementEarned: notificationRecieveConfig,
app: notificationRecieveConfig,
test: notificationRecieveConfig,
@@ -288,6 +290,7 @@ export default class extends Endpoint { // eslint-
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
if (ps.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility;
if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility;
+ if (ps.chatScope !== undefined) updates.chatScope = ps.chatScope;
function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) {
// TODO: ちゃんと数える
diff --git a/packages/backend/src/server/api/endpoints/invite/create.ts b/packages/backend/src/server/api/endpoints/invite/create.ts
index a70b587da7..f2e683ddf2 100644
--- a/packages/backend/src/server/api/endpoints/invite/create.ts
+++ b/packages/backend/src/server/api/endpoints/invite/create.ts
@@ -18,7 +18,7 @@ export const meta = {
tags: ['meta'],
requireCredential: true,
- requireRolePolicy: 'canInvite',
+ requiredRolePolicy: 'canInvite',
kind: 'write:invite-codes',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/invite/delete.ts b/packages/backend/src/server/api/endpoints/invite/delete.ts
index e960ff9f4e..06f47e90bc 100644
--- a/packages/backend/src/server/api/endpoints/invite/delete.ts
+++ b/packages/backend/src/server/api/endpoints/invite/delete.ts
@@ -14,7 +14,7 @@ export const meta = {
tags: ['meta'],
requireCredential: true,
- requireRolePolicy: 'canInvite',
+ requiredRolePolicy: 'canInvite',
kind: 'write:invite-codes',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/invite/limit.ts b/packages/backend/src/server/api/endpoints/invite/limit.ts
index 2ffd41ae28..0067dce231 100644
--- a/packages/backend/src/server/api/endpoints/invite/limit.ts
+++ b/packages/backend/src/server/api/endpoints/invite/limit.ts
@@ -15,7 +15,7 @@ export const meta = {
tags: ['meta'],
requireCredential: true,
- requireRolePolicy: 'canInvite',
+ requiredRolePolicy: 'canInvite',
kind: 'read:invite-codes',
res: {
diff --git a/packages/backend/src/server/api/endpoints/invite/list.ts b/packages/backend/src/server/api/endpoints/invite/list.ts
index 23aefe83a2..a99974a91e 100644
--- a/packages/backend/src/server/api/endpoints/invite/list.ts
+++ b/packages/backend/src/server/api/endpoints/invite/list.ts
@@ -14,7 +14,7 @@ export const meta = {
tags: ['meta'],
requireCredential: true,
- requireRolePolicy: 'canInvite',
+ requiredRolePolicy: 'canInvite',
kind: 'read:invite-codes',
res: {
diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts
index 0c6533d336..e73c98282c 100644
--- a/packages/backend/src/server/api/endpoints/notes/children.ts
+++ b/packages/backend/src/server/api/endpoints/notes/children.ts
@@ -71,8 +71,8 @@ export default class extends Endpoint { // eslint-
this.queryService.generateVisibilityQuery(query, me);
if (me) {
- this.queryService.generateMutedUserQuery(query, me);
- this.queryService.generateBlockedUserQuery(query, me);
+ this.queryService.generateMutedUserQueryForNotes(query, me);
+ this.queryService.generateBlockedUserQueryForNotes(query, me);
}
const notes = await query.limit(ps.limit).getMany();
diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
index 258a0bfb8f..8d38bb1c65 100644
--- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
@@ -79,8 +79,8 @@ export default class extends Endpoint { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
if (me) {
- this.queryService.generateMutedUserQuery(query, me);
- this.queryService.generateBlockedUserQuery(query, me);
+ this.queryService.generateMutedUserQueryForNotes(query, me);
+ this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
}
diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
index d514f5bb43..fb74b9a471 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -251,8 +251,8 @@ export default class extends Endpoint { // eslint-
}
this.queryService.generateVisibilityQuery(query, me);
- this.queryService.generateMutedUserQuery(query, me);
- this.queryService.generateBlockedUserQuery(query, me);
+ this.queryService.generateMutedUserQueryForNotes(query, me);
+ this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {
diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
index 9c4812cbbf..d530dad26c 100644
--- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -159,8 +159,8 @@ export default class extends Endpoint { // eslint-
this.queryService.generateVisibilityQuery(query, me);
if (me) {
- this.queryService.generateMutedUserQuery(query, me);
- this.queryService.generateBlockedUserQuery(query, me);
+ this.queryService.generateMutedUserQueryForNotes(query, me);
+ this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
const mutedChannelIds = await this.channelMutingService
diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts
index 5558dd3a8b..bbb63646e9 100644
--- a/packages/backend/src/server/api/endpoints/notes/mentions.ts
+++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts
@@ -9,7 +9,6 @@ import type { NotesRepository, FollowingsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
-import { NoteReadService } from '@/core/NoteReadService.js';
import { DI } from '@/di-symbols.js';
export const meta = {
@@ -52,7 +51,6 @@ export default class extends Endpoint { // eslint-
private noteEntityService: NoteEntityService,
private queryService: QueryService,
- private noteReadService: NoteReadService,
) {
super(meta, paramDef, async (ps, me) => {
const followingQuery = this.followingsRepository.createQueryBuilder('following')
@@ -74,9 +72,9 @@ export default class extends Endpoint { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
- this.queryService.generateMutedUserQuery(query, me);
+ this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateMutedNoteThreadQuery(query, me);
- this.queryService.generateBlockedUserQuery(query, me);
+ this.queryService.generateBlockedUserQueryForNotes(query, me);
if (ps.visibility) {
query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });
@@ -89,8 +87,6 @@ export default class extends Endpoint { // eslint-
const mentions = await query.limit(ps.limit).getMany();
- this.noteReadService.read(me.id, mentions);
-
return await this.noteEntityService.packMany(mentions, me);
});
}
diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts
index ffe1ee6eb8..b34d9261a1 100644
--- a/packages/backend/src/server/api/endpoints/notes/renotes.ts
+++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts
@@ -72,8 +72,8 @@ export default class extends Endpoint { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
- if (me) this.queryService.generateMutedUserQuery(query, me);
- if (me) this.queryService.generateBlockedUserQuery(query, me);
+ if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
+ if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
const renotes = await query.limit(ps.limit).getMany();
diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts
index 5f32332a6a..f36af1a328 100644
--- a/packages/backend/src/server/api/endpoints/notes/replies.ts
+++ b/packages/backend/src/server/api/endpoints/notes/replies.ts
@@ -56,8 +56,8 @@ export default class extends Endpoint { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
- if (me) this.queryService.generateMutedUserQuery(query, me);
- if (me) this.queryService.generateBlockedUserQuery(query, me);
+ if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
+ if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
const timeline = await query.limit(ps.limit).getMany();
diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
index 626ff080c7..c45851548a 100644
--- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
@@ -81,8 +81,8 @@ export default class extends Endpoint { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
- if (me) this.queryService.generateMutedUserQuery(query, me);
- if (me) this.queryService.generateBlockedUserQuery(query, me);
+ if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
+ if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
try {
if (ps.tag) {
diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts
index 732d644a29..29c6aa7434 100644
--- a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts
@@ -9,7 +9,6 @@ import type { NotesRepository, NoteThreadMutingsRepository } from '@/models/_.js
import { IdService } from '@/core/IdService.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js';
-import { NoteReadService } from '@/core/NoteReadService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
@@ -52,7 +51,6 @@ export default class extends Endpoint { // eslint-
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
private getterService: GetterService,
- private noteReadService: NoteReadService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -69,8 +67,6 @@ export default class extends Endpoint { // eslint-
}],
});
- await this.noteReadService.read(me.id, mutedNotes);
-
await this.noteThreadMutingsRepository.insert({
id: this.idService.gen(),
threadId: note.threadId ?? note.id,
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
index b745167ec8..7b995aefab 100644
--- a/packages/backend/src/server/api/endpoints/notes/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts
@@ -211,8 +211,8 @@ export default class extends Endpoint { // eslint-
}));
this.queryService.generateVisibilityQuery(query, me);
- this.queryService.generateMutedUserQuery(query, me);
- this.queryService.generateBlockedUserQuery(query, me);
+ this.queryService.generateMutedUserQueryForNotes(query, me);
+ this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {
diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
index d1bd27cf40..7c12d29fda 100644
--- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
@@ -186,8 +186,8 @@ export default class extends Endpoint { // eslint-
}));
this.queryService.generateVisibilityQuery(query, me);
- this.queryService.generateMutedUserQuery(query, me);
- this.queryService.generateBlockedUserQuery(query, me);
+ this.queryService.generateMutedUserQueryForNotes(query, me);
+ this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
// -- ミュートされたチャンネルのリノート対策
diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts
index b8a32ba71c..6edee35ba5 100644
--- a/packages/backend/src/server/api/endpoints/roles/notes.ts
+++ b/packages/backend/src/server/api/endpoints/roles/notes.ts
@@ -120,8 +120,8 @@ export default class extends Endpoint { // eslint-
}
this.queryService.generateVisibilityQuery(query, me);
- this.queryService.generateMutedUserQuery(query, me);
- this.queryService.generateBlockedUserQuery(query, me);
+ this.queryService.generateMutedUserQueryForNotes(query, me);
+ this.queryService.generateBlockedUserQueryForNotes(query, me);
const notes = await query.getMany();
notes.sort((a, b) => a.id > b.id ? -1 : 1);
diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts
index 72561dfa92..15acdae22c 100644
--- a/packages/backend/src/server/api/endpoints/users/notes.ts
+++ b/packages/backend/src/server/api/endpoints/users/notes.ts
@@ -207,8 +207,8 @@ export default class extends Endpoint { // eslint-
this.queryService.generateVisibilityQuery(query, me);
if (me) {
- this.queryService.generateMutedUserQuery(query, me, { id: ps.userId });
- this.queryService.generateBlockedUserQuery(query, me);
+ this.queryService.generateMutedUserQueryForNotes(query, me, { id: ps.userId });
+ this.queryService.generateBlockedUserQueryForNotes(query, me);
}
if (ps.withFiles) {
diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts
index 5b3b4527f7..5b1c6b514b 100644
--- a/packages/backend/src/server/api/endpoints/users/recommendation.ts
+++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts
@@ -63,7 +63,7 @@ export default class extends Endpoint { // eslint-
this.queryService.generateMutedUserQueryForUsers(query, me);
this.queryService.generateBlockQueryForUsers(query, me);
- this.queryService.generateBlockedUserQuery(query, me);
+ this.queryService.generateBlockedUserQueryForNotes(query, me);
const followingQuery = this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId')
diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
index 8ff952dcb5..134f1a8e87 100644
--- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
+++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
@@ -46,7 +46,7 @@ export default class extends Endpoint { // eslint-
private userSearchService: UserSearchService,
) {
super(meta, paramDef, (ps, me) => {
- return this.userSearchService.search({
+ return this.userSearchService.searchByUsernameAndHost({
username: ps.username,
host: ps.host,
}, {
diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts
index 0b0136066d..5d36847e03 100644
--- a/packages/backend/src/server/api/endpoints/users/search.ts
+++ b/packages/backend/src/server/api/endpoints/users/search.ts
@@ -3,14 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
-import type { MiUser } from '@/models/User.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
-import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
+import { UserSearchService } from '@/core/UserSearchService.js';
export const meta = {
tags: ['users'],
@@ -45,79 +42,15 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
- @Inject(DI.userProfilesRepository)
- private userProfilesRepository: UserProfilesRepository,
-
private userEntityService: UserEntityService,
+ private userSearchService: UserSearchService,
) {
super(meta, paramDef, async (ps, me) => {
- const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
-
- ps.query = ps.query.trim();
- const isUsername = ps.query.startsWith('@') && !ps.query.includes(' ') && ps.query.indexOf('@', 1) === -1;
-
- let users: MiUser[] = [];
-
- const nameQuery = this.usersRepository.createQueryBuilder('user')
- .where(new Brackets(qb => {
- qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
-
- if (isUsername) {
- qb.orWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' });
- } else if (this.userEntityService.validateLocalUsername(ps.query)) { // Also search username if it qualifies as username
- qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
- }
- }))
- .andWhere(new Brackets(qb => {
- qb
- .where('user.updatedAt IS NULL')
- .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
- }))
- .andWhere('user.isSuspended = FALSE');
-
- if (ps.origin === 'local') {
- nameQuery.andWhere('user.host IS NULL');
- } else if (ps.origin === 'remote') {
- nameQuery.andWhere('user.host IS NOT NULL');
- }
-
- users = await nameQuery
- .orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
- .limit(ps.limit)
- .offset(ps.offset)
- .getMany();
-
- if (users.length < ps.limit) {
- const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
- .select('prof.userId')
- .where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
-
- if (ps.origin === 'local') {
- profQuery.andWhere('prof.userHost IS NULL');
- } else if (ps.origin === 'remote') {
- profQuery.andWhere('prof.userHost IS NOT NULL');
- }
-
- const query = this.usersRepository.createQueryBuilder('user')
- .where(`user.id IN (${ profQuery.getQuery() })`)
- .andWhere(new Brackets(qb => {
- qb
- .where('user.updatedAt IS NULL')
- .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
- }))
- .andWhere('user.isSuspended = FALSE')
- .setParameters(profQuery.getParameters());
-
- users = users.concat(await query
- .orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
- .limit(ps.limit)
- .offset(ps.offset)
- .getMany(),
- );
- }
+ const users = await this.userSearchService.search(ps.query.trim(), me?.id ?? null, {
+ offset: ps.offset,
+ limit: ps.limit,
+ origin: ps.origin,
+ });
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
});
diff --git a/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts
index 9426318e34..7139715293 100644
--- a/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts
+++ b/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts
@@ -12,7 +12,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireRolePolicy: 'canManageCustomEmojis',
+ requiredRolePolicy: 'canManageCustomEmojis',
kind: 'read:admin:emoji',
res: {
diff --git a/packages/backend/src/server/api/stream/ChannelsService.ts b/packages/backend/src/server/api/stream/ChannelsService.ts
index 253409259f..c0ef589dea 100644
--- a/packages/backend/src/server/api/stream/ChannelsService.ts
+++ b/packages/backend/src/server/api/stream/ChannelsService.ts
@@ -19,6 +19,8 @@ import { AntennaChannelService } from './channels/antenna.js';
import { DriveChannelService } from './channels/drive.js';
import { HashtagChannelService } from './channels/hashtag.js';
import { RoleTimelineChannelService } from './channels/role-timeline.js';
+import { ChatUserChannelService } from './channels/chat-user.js';
+import { ChatRoomChannelService } from './channels/chat-room.js';
import { ReversiChannelService } from './channels/reversi.js';
import { ReversiGameChannelService } from './channels/reversi-game.js';
import { type MiChannelService } from './channel.js';
@@ -40,6 +42,8 @@ export class ChannelsService {
private serverStatsChannelService: ServerStatsChannelService,
private queueStatsChannelService: QueueStatsChannelService,
private adminChannelService: AdminChannelService,
+ private chatUserChannelService: ChatUserChannelService,
+ private chatRoomChannelService: ChatRoomChannelService,
private reversiChannelService: ReversiChannelService,
private reversiGameChannelService: ReversiGameChannelService,
) {
@@ -62,6 +66,8 @@ export class ChannelsService {
case 'serverStats': return this.serverStatsChannelService;
case 'queueStats': return this.queueStatsChannelService;
case 'admin': return this.adminChannelService;
+ case 'chatUser': return this.chatUserChannelService;
+ case 'chatRoom': return this.chatRoomChannelService;
case 'reversi': return this.reversiChannelService;
case 'reversiGame': return this.reversiGameChannelService;
diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts
index 6c0e252742..50b9f7a02b 100644
--- a/packages/backend/src/server/api/stream/Connection.ts
+++ b/packages/backend/src/server/api/stream/Connection.ts
@@ -7,7 +7,6 @@ import * as WebSocket from 'ws';
import type { MiUser } from '@/models/User.js';
import type { MiAccessToken } from '@/models/AccessToken.js';
import type { Packed } from '@/misc/json-schema.js';
-import type { NoteReadService } from '@/core/NoteReadService.js';
import type { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
@@ -47,7 +46,6 @@ export default class Connection {
constructor(
private channelsService: ChannelsService,
- private noteReadService: NoteReadService,
private notificationService: NotificationService,
private cacheService: CacheService,
private channelFollowingService: ChannelFollowingService,
@@ -131,7 +129,7 @@ export default class Connection {
case 'readNotification': this.onReadNotification(body); break;
case 'subNote': this.onSubscribeNote(body); break;
case 's': this.onSubscribeNote(body); break; // alias
- case 'sr': this.onSubscribeNote(body); this.readNote(body); break;
+ case 'sr': this.onSubscribeNote(body); break;
case 'unsubNote': this.onUnsubscribeNote(body); break;
case 'un': this.onUnsubscribeNote(body); break; // alias
case 'connect': this.onChannelConnectRequested(body); break;
@@ -166,19 +164,6 @@ export default class Connection {
if (note.renote) add(note.renote);
}
- @bindThis
- private readNote(body: JsonValue | undefined) {
- if (!isJsonObject(body)) return;
- const id = body.id;
-
- const note = this.cachedNotes.find(n => n.id === id);
- if (note == null) return;
-
- if (this.user && (note.userId !== this.user.id)) {
- this.noteReadService.read(this.user.id, [note]);
- }
- }
-
@bindThis
private onReadNotification(payload: JsonValue | undefined) {
this.notificationService.readAllNotification(this.user!.id);
diff --git a/packages/backend/src/server/api/stream/channels/chat-room.ts b/packages/backend/src/server/api/stream/channels/chat-room.ts
new file mode 100644
index 0000000000..eda333dd30
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/chat-room.ts
@@ -0,0 +1,78 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { bindThis } from '@/decorators.js';
+import type { GlobalEvents } from '@/core/GlobalEventService.js';
+import type { JsonObject } from '@/misc/json-value.js';
+import { ChatService } from '@/core/ChatService.js';
+import Channel, { type MiChannelService } from '../channel.js';
+
+class ChatRoomChannel extends Channel {
+ public readonly chName = 'chatRoom';
+ public static shouldShare = false;
+ public static requireCredential = true as const;
+ public static kind = 'read:chat';
+ private roomId: string;
+
+ constructor(
+ private chatService: ChatService,
+
+ id: string,
+ connection: Channel['connection'],
+ ) {
+ super(id, connection);
+ }
+
+ @bindThis
+ public async init(params: JsonObject) {
+ if (typeof params.roomId !== 'string') return;
+ this.roomId = params.roomId;
+
+ this.subscriber.on(`chatRoomStream:${this.roomId}`, this.onEvent);
+ }
+
+ @bindThis
+ private async onEvent(data: GlobalEvents['chatRoom']['payload']) {
+ this.send(data.type, data.body);
+ }
+
+ @bindThis
+ public onMessage(type: string, body: any) {
+ switch (type) {
+ case 'read':
+ if (this.roomId) {
+ this.chatService.readRoomChatMessage(this.user!.id, this.roomId);
+ }
+ break;
+ }
+ }
+
+ @bindThis
+ public dispose() {
+ this.subscriber.off(`chatRoomStream:${this.roomId}`, this.onEvent);
+ }
+}
+
+@Injectable()
+export class ChatRoomChannelService implements MiChannelService {
+ public readonly shouldShare = ChatRoomChannel.shouldShare;
+ public readonly requireCredential = ChatRoomChannel.requireCredential;
+ public readonly kind = ChatRoomChannel.kind;
+
+ constructor(
+ private chatService: ChatService,
+ ) {
+ }
+
+ @bindThis
+ public create(id: string, connection: Channel['connection']): ChatRoomChannel {
+ return new ChatRoomChannel(
+ this.chatService,
+ id,
+ connection,
+ );
+ }
+}
diff --git a/packages/backend/src/server/api/stream/channels/chat-user.ts b/packages/backend/src/server/api/stream/channels/chat-user.ts
new file mode 100644
index 0000000000..5323484ed7
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/chat-user.ts
@@ -0,0 +1,78 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { bindThis } from '@/decorators.js';
+import type { GlobalEvents } from '@/core/GlobalEventService.js';
+import type { JsonObject } from '@/misc/json-value.js';
+import { ChatService } from '@/core/ChatService.js';
+import Channel, { type MiChannelService } from '../channel.js';
+
+class ChatUserChannel extends Channel {
+ public readonly chName = 'chatUser';
+ public static shouldShare = false;
+ public static requireCredential = true as const;
+ public static kind = 'read:chat';
+ private otherId: string;
+
+ constructor(
+ private chatService: ChatService,
+
+ id: string,
+ connection: Channel['connection'],
+ ) {
+ super(id, connection);
+ }
+
+ @bindThis
+ public async init(params: JsonObject) {
+ if (typeof params.otherId !== 'string') return;
+ this.otherId = params.otherId;
+
+ this.subscriber.on(`chatUserStream:${this.user!.id}-${this.otherId}`, this.onEvent);
+ }
+
+ @bindThis
+ private async onEvent(data: GlobalEvents['chatUser']['payload']) {
+ this.send(data.type, data.body);
+ }
+
+ @bindThis
+ public onMessage(type: string, body: any) {
+ switch (type) {
+ case 'read':
+ if (this.otherId) {
+ this.chatService.readUserChatMessage(this.user!.id, this.otherId);
+ }
+ break;
+ }
+ }
+
+ @bindThis
+ public dispose() {
+ this.subscriber.off(`chatUserStream:${this.user!.id}-${this.otherId}`, this.onEvent);
+ }
+}
+
+@Injectable()
+export class ChatUserChannelService implements MiChannelService {
+ public readonly shouldShare = ChatUserChannel.shouldShare;
+ public readonly requireCredential = ChatUserChannel.requireCredential;
+ public readonly kind = ChatUserChannel.kind;
+
+ constructor(
+ private chatService: ChatService,
+ ) {
+ }
+
+ @bindThis
+ public create(id: string, connection: Channel['connection']): ChatUserChannel {
+ return new ChatUserChannel(
+ this.chatService,
+ id,
+ connection,
+ );
+ }
+}
diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts
index ed56fe0d40..795980821b 100644
--- a/packages/backend/src/server/api/stream/channels/global-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -50,6 +50,9 @@ class GlobalTimelineChannel extends Channel {
if (note.visibility !== 'public') return;
if (note.channelId != null) return;
+ if (note.user.requireSigninToViewContents && this.user == null) return;
+ if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return;
+ if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return;
if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts
index 491029f5de..2984e18774 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -53,6 +53,9 @@ class LocalTimelineChannel extends Channel {
if (note.user.host !== null) return;
if (note.visibility !== 'public') return;
if (note.channelId != null) return;
+ if (note.user.requireSigninToViewContents && this.user == null) return;
+ if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return;
+ if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return;
// 関係ない返信は除外
if (note.reply && this.user && !this.following[note.userId]?.withReplies && !this.withReplies) {
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index b55d327f86..24794cbf2a 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -127,11 +127,6 @@
document.documentElement.classList.add('useSystemFont');
}
- const wallpaper = localStorage.getItem('wallpaper');
- if (wallpaper) {
- document.documentElement.style.backgroundImage = `url(${wallpaper})`;
- }
-
const customCss = localStorage.getItem('customCss');
if (customCss && customCss.length > 0) {
const style = document.createElement('style');
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index c6b1035554..5d5f1e3b71 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -15,6 +15,7 @@
* receiveFollowRequest - フォローリクエストされた
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
* roleAssigned - ロールが付与された
+ * chatRoomInvitationReceived - チャットルームに招待された
* achievementEarned - 実績を獲得
* exportCompleted - エクスポートが完了
* login - ログイン
@@ -34,6 +35,7 @@ export const notificationTypes = [
'receiveFollowRequest',
'followRequestAccepted',
'roleAssigned',
+ 'chatRoomInvitationReceived',
'achievementEarned',
'exportCompleted',
'login',
@@ -122,6 +124,7 @@ export const moderationLogTypes = [
'deletePage',
'deleteFlash',
'deleteGalleryPost',
+ 'deleteChatRoom',
'updateProxyAccountDescription',
] as const;
@@ -375,6 +378,10 @@ export type ModerationLogPayloads = {
postUserUsername: string;
post: any;
};
+ deleteChatRoom: {
+ roomId: string;
+ room: any;
+ };
updateProxyAccountDescription: {
before: string | null;
after: string | null;
diff --git a/packages/backend/test-federation/test/timeline.test.ts b/packages/backend/test-federation/test/timeline.test.ts
index 2250bf4a42..00635e654b 100644
--- a/packages/backend/test-federation/test/timeline.test.ts
+++ b/packages/backend/test-federation/test/timeline.test.ts
@@ -24,7 +24,7 @@ describe('Timeline', () => {
});
type TimelineChannel = keyof Misskey.Channels & (`${string}Timeline` | 'antenna' | 'userList' | 'hashtag');
- type TimelineEndpoint = keyof Misskey.Endpoints & (`${string}timeline` | 'antennas/notes' | 'roles/notes' | 'notes/search-by-tag');
+ type TimelineEndpoint = keyof Misskey.Endpoints & (`notes/${string}timeline` | 'antennas/notes' | 'roles/notes' | 'notes/search-by-tag');
const timelineMap = new Map([
['antenna', 'antennas/notes'],
['globalTimeline', 'notes/global-timeline'],
diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts
index c888941f1c..930a0e7e25 100644
--- a/packages/backend/test/e2e/antennas.ts
+++ b/packages/backend/test/e2e/antennas.ts
@@ -153,6 +153,7 @@ describe('アンテナ', () => {
caseSensitive: false,
createdAt: new Date(response.createdAt).toISOString(),
excludeKeywords: [['']],
+ excludeNotesInSensitiveChannel: false,
hasUnreadNote: false,
isActive: true,
keywords: [['keyword']],
@@ -224,6 +225,8 @@ describe('アンテナ', () => {
{ parameters: () => ({ withReplies: true }) },
{ parameters: () => ({ withFile: false }) },
{ parameters: () => ({ withFile: true }) },
+ { parameters: () => ({ excludeNotesInSensitiveChannel: false }) },
+ { parameters: () => ({ excludeNotesInSensitiveChannel: true }) },
];
test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => {
const response = await successfulApiCall({
@@ -647,6 +650,42 @@ describe('アンテナ', () => {
assert.deepStrictEqual(response, expected);
});
+ test('が取得できること(センシティブチャンネルのノートを除く)', async () => {
+ const keyword = 'キーワード';
+ const antenna = await successfulApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam, keywords: [[keyword]], excludeNotesInSensitiveChannel: true },
+ user: alice,
+ });
+ const nonSensitiveChannel = await successfulApiCall({
+ endpoint: 'channels/create',
+ parameters: { name: 'test', isSensitive: false },
+ user: alice,
+ });
+ const sensitiveChannel = await successfulApiCall({
+ endpoint: 'channels/create',
+ parameters: { name: 'test', isSensitive: true },
+ user: alice,
+ });
+
+ const noteInLocal = await post(bob, { text: `test ${keyword}` });
+ const noteInNonSensitiveChannel = await post(bob, { text: `test ${keyword}`, channelId: nonSensitiveChannel.id });
+ await post(bob, { text: `test ${keyword}`, channelId: sensitiveChannel.id });
+
+ const response = await successfulApiCall({
+ endpoint: 'antennas/notes',
+ parameters: { antennaId: antenna.id },
+ user: alice,
+ });
+ // 最後に投稿したものが先頭に来る。
+ const expected = [
+ noteInNonSensitiveChannel,
+ noteInLocal,
+ ];
+ assert.deepStrictEqual(response, expected);
+ });
+
+
test.skip('が取得でき、日付指定のPaginationに一貫性があること', async () => { });
test.each([
{ label: 'ID指定', offsetBy: 'id' },
diff --git a/packages/backend/test/e2e/mute.ts b/packages/backend/test/e2e/mute.ts
index f37da288b7..b464c24287 100644
--- a/packages/backend/test/e2e/mute.ts
+++ b/packages/backend/test/e2e/mute.ts
@@ -51,30 +51,8 @@ describe('Mute', () => {
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
});
- test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => {
- // 状態リセット
- await api('i/read-all-unread-notes', {}, alice);
-
- await post(carol, { text: '@alice hi' });
-
- const res = await api('i', {}, alice);
-
- assert.strictEqual(res.status, 200);
- assert.strictEqual(res.body.hasUnreadMentions, false);
- });
-
- test('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => {
- // 状態リセット
- await api('i/read-all-unread-notes', {}, alice);
-
- const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention');
-
- assert.strictEqual(fired, false);
- });
-
test('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => {
// 状態リセット
- await api('i/read-all-unread-notes', {}, alice);
await api('notifications/mark-all-as-read', {}, alice);
const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification');
diff --git a/packages/backend/test/e2e/thread-mute.ts b/packages/backend/test/e2e/thread-mute.ts
index 1ac99df884..1edc178fc2 100644
--- a/packages/backend/test/e2e/thread-mute.ts
+++ b/packages/backend/test/e2e/thread-mute.ts
@@ -38,48 +38,6 @@ describe('Note thread mute', () => {
assert.strictEqual(res.body.some(note => note.id === carolReplyWithoutMention.id), false);
});
- test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => {
- // 状態リセット
- await api('i/read-all-unread-notes', {}, alice);
-
- const bobNote = await post(bob, { text: '@alice @carol root note' });
-
- await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
-
- const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
-
- const res = await api('i', {}, alice);
-
- assert.strictEqual(res.status, 200);
- assert.strictEqual(res.body.hasUnreadMentions, false);
- });
-
- test('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => {
- // 状態リセット
- await api('i/read-all-unread-notes', {}, alice);
-
- const bobNote = await post(bob, { text: '@alice @carol root note' });
-
- await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
-
- let fired = false;
-
- const ws = await connectStream(alice, 'main', async ({ type, body }) => {
- if (type === 'unreadMention') {
- if (body === bobNote.id) return;
- fired = true;
- }
- });
-
- const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
-
- setTimeout(() => {
- assert.strictEqual(fired, false);
- ws.close();
- done();
- }, 5000);
- }));
-
test('i/notifications にミュートしているスレッドの通知が含まれない', async () => {
const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts
index 822ca14ae6..a342bba64c 100644
--- a/packages/backend/test/e2e/users.ts
+++ b/packages/backend/test/e2e/users.ts
@@ -15,7 +15,7 @@ describe('ユーザー', () => {
// エンティティとしてのユーザーを主眼においたテストを記述する
// (Userを返すエンドポイントとUserエンティティを書き換えるエンドポイントをテストする)
- const stripUndefined = (orig: T): Partial => {
+ const stripUndefined = (orig: T): Partial => {
return Object.entries({ ...orig })
.filter(([, value]) => value !== undefined)
.reduce((obj: Partial, [key, value]) => {
@@ -83,6 +83,8 @@ describe('ユーザー', () => {
publicReactions: user.publicReactions,
followingVisibility: user.followingVisibility,
followersVisibility: user.followersVisibility,
+ chatScope: user.chatScope,
+ canChat: user.canChat,
roles: user.roles,
memo: user.memo,
});
@@ -132,6 +134,7 @@ describe('ユーザー', () => {
hasUnreadAnnouncement: user.hasUnreadAnnouncement,
hasUnreadAntenna: user.hasUnreadAntenna,
hasUnreadChannel: user.hasUnreadChannel,
+ hasUnreadChatMessages: user.hasUnreadChatMessages,
hasUnreadNotification: user.hasUnreadNotification,
unreadNotificationsCount: user.unreadNotificationsCount,
hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest,
@@ -343,6 +346,8 @@ describe('ユーザー', () => {
assert.strictEqual(response.publicReactions, true);
assert.strictEqual(response.followingVisibility, 'public');
assert.strictEqual(response.followersVisibility, 'public');
+ assert.strictEqual(response.chatScope, 'mutual');
+ assert.strictEqual(response.canChat, true);
assert.deepStrictEqual(response.roles, []);
assert.strictEqual(response.memo, null);
@@ -369,6 +374,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.hasUnreadAnnouncement, false);
assert.strictEqual(response.hasUnreadAntenna, false);
assert.strictEqual(response.hasUnreadChannel, false);
+ assert.strictEqual(response.hasUnreadChatMessages, false);
assert.strictEqual(response.hasUnreadNotification, false);
assert.strictEqual(response.unreadNotificationsCount, 0);
assert.strictEqual(response.hasPendingReceivedFollowRequest, false);
@@ -728,7 +734,7 @@ describe('ユーザー', () => {
});
test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
- { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
+ { label: 'ミュートユーザーが含まれない', user: () => userMutedByAlice, excluded: true },
{ label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: () => userLocking },
diff --git a/packages/backend/test/unit/UserSearchService.ts b/packages/backend/test/unit/UserSearchService.ts
index 66a7f39ff1..697425beb8 100644
--- a/packages/backend/test/unit/UserSearchService.ts
+++ b/packages/backend/test/unit/UserSearchService.ts
@@ -134,13 +134,13 @@ describe('UserSearchService', () => {
await app.close();
});
- describe('search', () => {
+ describe('searchByUsernameAndHost', () => {
test('フォロー中のアクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
await setActive([alice, alyce, alyssa, bob, bobbi, bobbie, bobby]);
await setInactive([alycia, alysha, alyson]);
- const result = await service.search(
+ const result = await service.searchByUsernameAndHost(
{ username: 'al' },
{ limit: 100 },
root,
@@ -154,7 +154,7 @@ describe('UserSearchService', () => {
await createFollowings(root, [alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
- const result = await service.search(
+ const result = await service.searchByUsernameAndHost(
{ username: 'al' },
{ limit: 100 },
root,
@@ -168,7 +168,7 @@ describe('UserSearchService', () => {
await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
await setInactive([alice, alyce, alycia]);
- const result = await service.search(
+ const result = await service.searchByUsernameAndHost(
{ username: 'al' },
{ limit: 100 },
root,
@@ -181,7 +181,7 @@ describe('UserSearchService', () => {
test('フォローしていない非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
- const result = await service.search(
+ const result = await service.searchByUsernameAndHost(
{ username: 'al' },
{ limit: 100 },
root,
@@ -195,7 +195,7 @@ describe('UserSearchService', () => {
await setActive([root, alyssa, bob, bobbi, alyce, alycia]);
await setInactive([alyson, alice, alysha, bobbie, bobby]);
- const result = await service.search(
+ const result = await service.searchByUsernameAndHost(
{ },
{ limit: 100 },
root,
@@ -216,7 +216,7 @@ describe('UserSearchService', () => {
await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
await setInactive([alice, alyce, alycia]);
- const result = await service.search(
+ const result = await service.searchByUsernameAndHost(
{ username: 'al' },
{ limit: 100 },
);
@@ -228,7 +228,7 @@ describe('UserSearchService', () => {
test('[非ログイン] 非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
- const result = await service.search(
+ const result = await service.searchByUsernameAndHost(
{ username: 'al' },
{ limit: 100 },
);
@@ -240,7 +240,7 @@ describe('UserSearchService', () => {
await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
- const result = await service.search(
+ const result = await service.searchByUsernameAndHost(
{ username: 'al', host: 'exam' },
{ limit: 100 },
root,
@@ -253,7 +253,7 @@ describe('UserSearchService', () => {
await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
await setSuspended([alice, alyce, alycia]);
- const result = await service.search(
+ const result = await service.searchByUsernameAndHost(
{ username: 'al' },
{ limit: 100 },
root,
diff --git a/packages/backend/test/unit/WebhookTestService.ts b/packages/backend/test/unit/WebhookTestService.ts
index 45bc932469..736aac40b4 100644
--- a/packages/backend/test/unit/WebhookTestService.ts
+++ b/packages/backend/test/unit/WebhookTestService.ts
@@ -14,6 +14,7 @@ import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersReposi
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import { QueueService } from '@/core/QueueService.js';
+import { CustomEmojiService } from '@/core/CustomEmojiService.js';
describe('WebhookTestService', () => {
let app: TestingModule;
@@ -56,6 +57,11 @@ describe('WebhookTestService', () => {
providers: [
WebhookTestService,
IdService,
+ {
+ provide: CustomEmojiService, useFactory: () => ({
+ populateEmojis: jest.fn(),
+ }),
+ },
{
provide: QueueService, useFactory: () => ({
systemWebhookDeliver: jest.fn(),
diff --git a/packages/backend/test/unit/entities/UserEntityService.ts b/packages/backend/test/unit/entities/UserEntityService.ts
index e4f42809f8..6b7eedff55 100644
--- a/packages/backend/test/unit/entities/UserEntityService.ts
+++ b/packages/backend/test/unit/entities/UserEntityService.ts
@@ -50,6 +50,7 @@ import { AccountMoveService } from '@/core/AccountMoveService.js';
import { ReactionService } from '@/core/ReactionService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
+import { ChatService } from '@/core/ChatService.js';
process.env.NODE_ENV = 'test';
@@ -172,6 +173,7 @@ describe('UserEntityService', () => {
ReactionService,
ReactionsBufferingService,
NotificationService,
+ ChatService,
];
app = await Test.createTestingModule({
diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json
index 21247e32ab..07da959921 100644
--- a/packages/frontend-embed/package.json
+++ b/packages/frontend-embed/package.json
@@ -16,7 +16,7 @@
"@rollup/pluginutils": "5.1.4",
"@tabler/icons-webfont": "3.31.0",
"@twemoji/parser": "15.1.1",
- "@vitejs/plugin-vue": "5.2.1",
+ "@vitejs/plugin-vue": "5.2.3",
"@vue/compiler-sfc": "3.5.13",
"astring": "1.9.0",
"buraha": "0.0.1",
@@ -25,46 +25,46 @@
"misskey-js": "workspace:*",
"frontend-shared": "workspace:*",
"punycode.js": "2.3.1",
- "rollup": "4.34.9",
- "sass": "1.85.1",
- "shiki": "3.1.0",
+ "rollup": "4.39.0",
+ "sass": "1.86.3",
+ "shiki": "3.2.2",
"tinycolor2": "1.6.0",
- "tsc-alias": "1.8.11",
+ "tsc-alias": "1.8.15",
"tsconfig-paths": "4.2.0",
- "typescript": "5.8.2",
+ "typescript": "5.8.3",
"uuid": "11.1.0",
"json5": "2.2.3",
- "vite": "6.2.1",
+ "vite": "6.2.4",
"vue": "3.5.13"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.0",
"@testing-library/vue": "8.1.0",
- "@types/estree": "1.0.6",
+ "@types/estree": "1.0.7",
"@types/micromatch": "4.0.9",
- "@types/node": "22.13.9",
+ "@types/node": "22.14.0",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
- "@types/ws": "8.18.0",
- "@typescript-eslint/eslint-plugin": "8.26.0",
- "@typescript-eslint/parser": "8.26.0",
- "@vitest/coverage-v8": "3.0.8",
+ "@types/ws": "8.18.1",
+ "@typescript-eslint/eslint-plugin": "8.29.1",
+ "@typescript-eslint/parser": "8.29.1",
+ "@vitest/coverage-v8": "3.1.1",
"@vue/runtime-core": "3.5.13",
"acorn": "8.14.1",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "10.0.0",
"fast-glob": "3.3.3",
- "happy-dom": "17.3.0",
+ "happy-dom": "17.4.4",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"msw": "2.7.3",
"nodemon": "3.1.9",
"prettier": "3.5.3",
- "start-server-and-test": "2.0.10",
+ "start-server-and-test": "2.0.11",
"vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "2.2.8",
- "vue-eslint-parser": "10.1.1",
+ "vue-eslint-parser": "10.1.3",
"vue-tsc": "2.2.8"
}
}
diff --git a/packages/frontend-embed/src/components/EmImgWithBlurhash.vue b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue
index bf976c71ae..0bff048ce4 100644
--- a/packages/frontend-embed/src/components/EmImgWithBlurhash.vue
+++ b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue
@@ -33,13 +33,11 @@ const canvasPromise = new Promise(resol
Math.min(navigator.hardwareConcurrency - 1, 4),
);
resolve(workers);
- if (_DEV_) console.log('WebGL2 in worker is supported!');
} else {
const canvas = document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
resolve(canvas);
- if (_DEV_) console.log('WebGL2 in worker is not supported...');
}
testWorker.terminate();
});
diff --git a/packages/frontend-embed/src/components/EmMediaImage.vue b/packages/frontend-embed/src/components/EmMediaImage.vue
index d711020a74..2c96ce3215 100644
--- a/packages/frontend-embed/src/components/EmMediaImage.vue
+++ b/packages/frontend-embed/src/components/EmMediaImage.vue
@@ -95,7 +95,7 @@ async function onclick(ev: MouseEvent) {
position: absolute;
border-radius: 6px;
background-color: var(--MI_THEME-fg);
- color: var(--MI_THEME-accentLighten);
+ color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
font-size: 12px;
opacity: .5;
padding: 5px 8px;
@@ -153,7 +153,7 @@ html[data-color-scheme=light] .visible {
/* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */
background-color: black;
border-radius: 6px;
- color: var(--MI_THEME-accentLighten);
+ color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
display: inline-block;
font-weight: bold;
font-size: 0.8em;
diff --git a/packages/frontend-embed/src/components/EmPagination.vue b/packages/frontend-embed/src/components/EmPagination.vue
index 5d5317a912..94a91305f4 100644
--- a/packages/frontend-embed/src/components/EmPagination.vue
+++ b/packages/frontend-embed/src/components/EmPagination.vue
@@ -34,10 +34,11 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -420,7 +420,7 @@ onBeforeUnmount(() => {
}
&:active {
- background: var(--MI_THEME-accentDarken);
+ background: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
color: #fff !important;
}
}
diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue
index c81edc2a73..891af7f696 100644
--- a/packages/frontend/src/components/MkButton.vue
+++ b/packages/frontend/src/components/MkButton.vue
@@ -7,11 +7,11 @@ SPDX-License-Identifier: AGPL-3.0-only
[props.instanceUrl, props.sitekey, props.secretKey], async () => {
if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') {
available.value = true;
} else if (src.value !== null) {
- (document.getElementById(scriptId.value) ?? document.head.appendChild(Object.assign(document.createElement('script'), {
+ (window.document.getElementById(scriptId.value) ?? window.document.head.appendChild(Object.assign(window.document.createElement('script'), {
async: true,
id: scriptId.value,
src: src.value,
@@ -149,7 +149,7 @@ async function requestRender() {
if (captcha.value.render && captchaEl.value instanceof Element && props.sitekey) {
// reCAPTCHAのレンダリング重複判定を回避するため、captchaEl配下に仮のdivを用意する.
// (同じdivに対して複数回renderを呼び出すとreCAPTCHAはエラーを返すので)
- const elem = document.createElement('div');
+ const elem = window.document.createElement('div');
captchaEl.value.appendChild(elem);
captchaWidgetId.value = captcha.value.render(elem, {
@@ -174,7 +174,7 @@ async function requestRender() {
function clearWidget() {
if (props.provider === 'mcaptcha') {
- const container = document.getElementById('mcaptcha__widget-container');
+ const container = window.document.getElementById('mcaptcha__widget-container');
if (container) {
container.innerHTML = '';
}
diff --git a/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts
index a42e80c27a..4304c2e2b7 100644
--- a/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts
+++ b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts
@@ -2,20 +2,18 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* 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/addon-actions';
import { expect, userEvent, within } from '@storybook/test';
import { channel } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkChannelFollowButton from './MkChannelFollowButton.vue';
+import type { StoryObj } from '@storybook/vue3';
import { i18n } from '@/i18n.js';
function sleep(ms: number) {
- return new Promise(resolve => setTimeout(resolve, ms));
+ return new Promise(resolve => window.setTimeout(resolve, ms));
}
export const Default = {
diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue
index 2ef2d84198..1aec8d0c07 100644
--- a/packages/frontend/src/components/MkChannelFollowButton.vue
+++ b/packages/frontend/src/components/MkChannelFollowButton.vue
@@ -103,13 +103,13 @@ async function onClick() {
background: var(--MI_THEME-accent);
&:hover {
- background: var(--MI_THEME-accentLighten);
- border-color: var(--MI_THEME-accentLighten);
+ background: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
+ border-color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
}
&:active {
- background: var(--MI_THEME-accentDarken);
- border-color: var(--MI_THEME-accentDarken);
+ background: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
+ border-color: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
}
}
diff --git a/packages/frontend/src/components/MkClickerGame.stories.impl.ts b/packages/frontend/src/components/MkClickerGame.stories.impl.ts
index eb7e61f294..6e1eb13d61 100644
--- a/packages/frontend/src/components/MkClickerGame.stories.impl.ts
+++ b/packages/frontend/src/components/MkClickerGame.stories.impl.ts
@@ -2,18 +2,16 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* 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/addon-actions';
import { expect, userEvent, within } from '@storybook/test';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkClickerGame from './MkClickerGame.vue';
+import type { StoryObj } from '@storybook/vue3';
function sleep(ms: number) {
- return new Promise(resolve => setTimeout(resolve, ms));
+ return new Promise(resolve => window.setTimeout(resolve, ms));
}
export const Default = {
diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue
index 46fdf15b5d..bdb2ba6a44 100644
--- a/packages/frontend/src/components/MkCodeEditor.vue
+++ b/packages/frontend/src/components/MkCodeEditor.vue
@@ -140,7 +140,7 @@ watch(v, newValue => {
.caption {
font-size: 0.85em;
padding: 8px 0 0 0;
- color: var(--MI_THEME-fgTransparentWeak);
+ color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
&:empty {
display: none;
diff --git a/packages/frontend/src/components/MkColorInput.vue b/packages/frontend/src/components/MkColorInput.vue
index 50931cc318..80618ebfe4 100644
--- a/packages/frontend/src/components/MkColorInput.vue
+++ b/packages/frontend/src/components/MkColorInput.vue
@@ -60,7 +60,7 @@ const onInput = () => {
.caption {
font-size: 0.85em;
padding: 8px 0 0 0;
- color: var(--MI_THEME-fgTransparentWeak);
+ color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
&:empty {
display: none;
diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue
index 39ca39aad7..1993991106 100644
--- a/packages/frontend/src/components/MkContainer.vue
+++ b/packages/frontend/src/components/MkContainer.vue
@@ -181,11 +181,16 @@ onUnmounted(() => {
left: 0;
color: var(--MI_THEME-panelHeaderFg);
background: var(--MI_THEME-panelHeaderBg);
- border-bottom: solid 0.5px var(--MI_THEME-panelHeaderDivider);
z-index: 2;
line-height: 1.4em;
}
+@container style(--MI_THEME-panelHeaderBg: var(--MI_THEME-panel)) {
+ .header {
+ box-shadow: 0 0.5px 0 0 light-dark(#0002, #fff2);
+ }
+}
+
.title {
margin: 0;
padding: 12px 16px;
@@ -215,6 +220,14 @@ onUnmounted(() => {
.content {
--MI-stickyTop: 0px;
+ /*
+ 理屈は知らないけど、ここでbackgroundを設定しておかないと
+ スクロールコンテナーが少なくともChromeにおいて
+ main thread scrolling になってしまい、パフォーマンスが(多分)落ちる。
+ backgroundが透明だと裏側を描画しないといけなくなるとかそういう理由かもしれない
+ */
+ background: var(--MI_THEME-panel);
+
&.omitted {
position: relative;
max-height: var(--maxHeight);
diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue
index e47dba4bae..9c6397a72c 100644
--- a/packages/frontend/src/components/MkContextMenu.vue
+++ b/packages/frontend/src/components/MkContextMenu.vue
@@ -68,11 +68,11 @@ onMounted(() => {
rootEl.value.style.left = `${left}px`;
}
- document.body.addEventListener('mousedown', onMousedown);
+ window.document.body.addEventListener('mousedown', onMousedown);
});
onBeforeUnmount(() => {
- document.body.removeEventListener('mousedown', onMousedown);
+ window.document.body.removeEventListener('mousedown', onMousedown);
});
function onMousedown(evt: Event) {
diff --git a/packages/frontend/src/components/MkCropperDialog.stories.impl.ts b/packages/frontend/src/components/MkCropperDialog.stories.impl.ts
index 27ce60415b..78cb4120de 100644
--- a/packages/frontend/src/components/MkCropperDialog.stories.impl.ts
+++ b/packages/frontend/src/components/MkCropperDialog.stories.impl.ts
@@ -3,14 +3,12 @@
* 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/addon-actions';
import { file } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkCropperDialog from './MkCropperDialog.vue';
+import type { StoryObj } from '@storybook/vue3';
export const Default = {
render(args) {
return {
@@ -55,7 +53,7 @@ export const Default = {
http.get('/proxy/image.webp', async ({ request }) => {
const url = new URL(request.url).searchParams.get('url');
if (url === 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true') {
- const image = await (await fetch('client-assets/fedi.jpg')).blob();
+ const image = await (await window.fetch('client-assets/fedi.jpg')).blob();
return new HttpResponse(image, {
headers: {
'Content-Type': 'image/jpeg',
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index bd2e6a9cbc..ba21394cbc 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -122,7 +122,7 @@ onMounted(() => {
cropper = new Cropper(imgEl.value!, {
});
- const computedStyle = getComputedStyle(document.documentElement);
+ const computedStyle = getComputedStyle(window.document.documentElement);
const selection = cropper.getCropperSelection()!;
selection.themeColor = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue
index b5842876ac..1cf6f0b744 100644
--- a/packages/frontend/src/components/MkDateSeparatedList.vue
+++ b/packages/frontend/src/components/MkDateSeparatedList.vue
@@ -3,16 +3,18 @@ SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
+
+
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index 2824701579..13ffd6b7cc 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.t_exportCompleted]: notification.type === 'exportCompleted',
[$style.t_login]: notification.type === 'login',
[$style.t_createToken]: notification.type === 'createToken',
+ [$style.t_chatRoomInvitationReceived]: notification.type === 'chatRoomInvitationReceived',
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
}]"
>
@@ -43,6 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
+
@@ -61,6 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._notification.pollEnded }}
{{ i18n.ts._notification.newNote }}:
{{ i18n.ts._notification.roleAssigned }}
+ {{ i18n.ts._notification.chatRoomInvitationReceived }}
{{ i18n.ts._notification.achievementEarned }}
{{ i18n.ts._notification.login }}
{{ i18n.ts._notification.createToken }}
@@ -104,6 +107,9 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ notification.role.name }}
+
+ {{ notification.invitation.room.name }}
+
{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
@@ -369,6 +375,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
pointer-events: none;
}
+.t_chatRoomInvitationReceived {
+ padding: 3px;
+ background: var(--eventOther);
+ pointer-events: none;
+}
+
.tail {
flex: 1;
min-width: 0;
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 08fc846327..99eca35eb7 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -14,22 +14,31 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
-
-
+
+
+
+
+
+
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 5cb00c5292..32c2e48b01 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -117,7 +117,7 @@ windowRouter.addListener('change', ctx => {
windowRouter.init();
provide(DI.router, windowRouter);
-provide('inAppSearchMarkerId', searchMarkerId);
+provide(DI.inAppSearchMarkerId, searchMarkerId);
provideMetadataReceiver((metadataGetter) => {
const info = metadataGetter();
pageMetadata.value = info;
@@ -125,7 +125,7 @@ provideMetadataReceiver((metadataGetter) => {
provideReactiveMetadata(pageMetadata);
provide('shouldOmitHeaderTitle', true);
provide('shouldHeaderThin', true);
-provide('forceSpacerMin', true);
+provide(DI.forceSpacerMin, true);
const contextmenu = computed(() => ([{
icon: 'ti ti-player-eject',
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index 6a1a91a9f4..9adc3d98da 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-