summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-06-05 19:47:08 +0900
committerGitHub <noreply@github.com>2023-06-05 19:47:08 +0900
commit407a965c1d78db9b13ec89a7be910b3c120aafcf (patch)
tree33e00f7a00c4e33b2c95a6e2aba85cea7b9f05f6 /packages/backend/src/server/api
parentMerge pull request #10833 from misskey-dev/develop (diff)
parentMerge branch 'develop' of https://github.com/misskey-dev/misskey into develop (diff)
downloadmisskey-407a965c1d78db9b13ec89a7be910b3c120aafcf.tar.gz
misskey-407a965c1d78db9b13ec89a7be910b3c120aafcf.tar.bz2
misskey-407a965c1d78db9b13ec89a7be910b3c120aafcf.zip
Merge pull request #10932 from misskey-dev/develop
Release: 13.13.0
Diffstat (limited to 'packages/backend/src/server/api')
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts7
-rw-r--r--packages/backend/src/server/api/AuthenticateService.ts2
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts12
-rw-r--r--packages/backend/src/server/api/StreamingApiServerService.ts136
-rw-r--r--packages/backend/src/server/api/endpoints.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/announcements/update.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/add.ts31
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/update.ts27
-rw-r--r--packages/backend/src/server/api/endpoints/admin/relays/add.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/notes.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/auth/accept.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/key-done.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/apps.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/i/notifications.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/meta.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/notes/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/global-timeline.ts9
-rw-r--r--packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts9
-rw-r--r--packages/backend/src/server/api/endpoints/notes/local-timeline.ts9
-rw-r--r--packages/backend/src/server/api/endpoints/notes/search-by-tag.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/notes/timeline.ts9
-rw-r--r--packages/backend/src/server/api/endpoints/reset-db.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/roles/notes.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts148
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/favorite.ts70
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/list.ts45
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/show.ts35
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts63
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/update.ts5
-rw-r--r--packages/backend/src/server/api/stream/channels/global-timeline.ts5
-rw-r--r--packages/backend/src/server/api/stream/channels/home-timeline.ts5
-rw-r--r--packages/backend/src/server/api/stream/channels/hybrid-timeline.ts5
-rw-r--r--packages/backend/src/server/api/stream/channels/local-timeline.ts5
-rw-r--r--packages/backend/src/server/api/stream/channels/role-timeline.ts11
-rw-r--r--packages/backend/src/server/api/stream/index.ts23
36 files changed, 585 insertions, 135 deletions
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index e3483c82c6..dad1a4132a 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -359,7 +359,12 @@ export class ApiCallService implements OnApplicationShutdown {
}
@bindThis
- public onApplicationShutdown(signal?: string | undefined) {
+ public dispose(): void {
clearInterval(this.userIpHistoriesClearIntervalId);
}
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
}
diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts
index 6548c475b2..e23591d876 100644
--- a/packages/backend/src/server/api/AuthenticateService.ts
+++ b/packages/backend/src/server/api/AuthenticateService.ts
@@ -36,7 +36,7 @@ export class AuthenticateService {
}
@bindThis
- public async authenticate(token: string | null | undefined): Promise<[LocalUser | null | undefined, AccessToken | null | undefined]> {
+ public async authenticate(token: string | null | undefined): Promise<[LocalUser | null, AccessToken | null]> {
if (token == null) {
return [null, null];
}
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index ee1aae5b6c..1e32e9988d 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -321,6 +321,9 @@ import * as ep___users_lists_pull from './endpoints/users/lists/pull.js';
import * as ep___users_lists_push from './endpoints/users/lists/push.js';
import * as ep___users_lists_show from './endpoints/users/lists/show.js';
import * as ep___users_lists_update from './endpoints/users/lists/update.js';
+import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js';
+import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
+import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js';
import * as ep___users_notes from './endpoints/users/notes.js';
import * as ep___users_pages from './endpoints/users/pages.js';
import * as ep___users_reactions from './endpoints/users/reactions.js';
@@ -659,6 +662,9 @@ const $users_lists_pull: Provider = { provide: 'ep:users/lists/pull', useClass:
const $users_lists_push: Provider = { provide: 'ep:users/lists/push', useClass: ep___users_lists_push.default };
const $users_lists_show: Provider = { provide: 'ep:users/lists/show', useClass: ep___users_lists_show.default };
const $users_lists_update: Provider = { provide: 'ep:users/lists/update', useClass: ep___users_lists_update.default };
+const $users_lists_favorite: Provider = { provide: 'ep:users/lists/favorite', useClass: ep___users_lists_favorite.default };
+const $users_lists_unfavorite: Provider = { provide: 'ep:users/lists/unfavorite', useClass: ep___users_lists_unfavorite.default };
+const $users_lists_create_from_public: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_create_from_public.default };
const $users_notes: Provider = { provide: 'ep:users/notes', useClass: ep___users_notes.default };
const $users_pages: Provider = { provide: 'ep:users/pages', useClass: ep___users_pages.default };
const $users_reactions: Provider = { provide: 'ep:users/reactions', useClass: ep___users_reactions.default };
@@ -1001,6 +1007,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_lists_push,
$users_lists_show,
$users_lists_update,
+ $users_lists_favorite,
+ $users_lists_unfavorite,
+ $users_lists_create_from_public,
$users_notes,
$users_pages,
$users_reactions,
@@ -1335,6 +1344,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_lists_push,
$users_lists_show,
$users_lists_update,
+ $users_lists_favorite,
+ $users_lists_unfavorite,
+ $users_lists_create_from_public,
$users_notes,
$users_pages,
$users_reactions,
diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts
index 258e8de034..893dfe956e 100644
--- a/packages/backend/src/server/api/StreamingApiServerService.ts
+++ b/packages/backend/src/server/api/StreamingApiServerService.ts
@@ -1,23 +1,27 @@
import { EventEmitter } from 'events';
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
-import * as websocket from 'websocket';
+import * as WebSocket from 'ws';
import { DI } from '@/di-symbols.js';
-import type { UsersRepository, BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, RenoteMutingsRepository } from '@/models/index.js';
+import type { UsersRepository, AccessToken } from '@/models/index.js';
import type { Config } from '@/config.js';
import { NoteReadService } from '@/core/NoteReadService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
-import { AuthenticateService } from './AuthenticateService.js';
+import { LocalUser } from '@/models/entities/User';
+import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
import MainStreamConnection from './stream/index.js';
import { ChannelsService } from './stream/ChannelsService.js';
-import type { ParsedUrlQuery } from 'querystring';
import type * as http from 'node:http';
@Injectable()
export class StreamingApiServerService {
+ #wss: WebSocket.WebSocketServer;
+ #connections = new Map<WebSocket.WebSocket, number>();
+ #cleanConnectionsIntervalId: NodeJS.Timeout | null = null;
+
constructor(
@Inject(DI.config)
private config: Config,
@@ -28,24 +32,6 @@ export class StreamingApiServerService {
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
- @Inject(DI.followingsRepository)
- private followingsRepository: FollowingsRepository,
-
- @Inject(DI.mutingsRepository)
- private mutingsRepository: MutingsRepository,
-
- @Inject(DI.renoteMutingsRepository)
- private renoteMutingsRepository: RenoteMutingsRepository,
-
- @Inject(DI.blockingsRepository)
- private blockingsRepository: BlockingsRepository,
-
- @Inject(DI.channelFollowingsRepository)
- private channelFollowingsRepository: ChannelFollowingsRepository,
-
- @Inject(DI.userProfilesRepository)
- private userProfilesRepository: UserProfilesRepository,
-
private cacheService: CacheService,
private noteReadService: NoteReadService,
private authenticateService: AuthenticateService,
@@ -55,25 +41,65 @@ export class StreamingApiServerService {
}
@bindThis
- public attachStreamingApi(server: http.Server) {
- // Init websocket server
- const ws = new websocket.server({
- httpServer: server,
+ public attach(server: http.Server): void {
+ this.#wss = new WebSocket.WebSocketServer({
+ noServer: true,
});
- ws.on('request', async (request) => {
- const q = request.resourceURL.query as ParsedUrlQuery;
+ server.on('upgrade', async (request, socket, head) => {
+ if (request.url == null) {
+ socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
+ socket.destroy();
+ return;
+ }
+
+ const q = new URL(request.url, `http://${request.headers.host}`).searchParams;
+
+ let user: LocalUser | null = null;
+ let app: AccessToken | null = null;
- // TODO: トークンが間違ってるなどしてauthenticateに失敗したら
- // コネクション切断するなりエラーメッセージ返すなりする
- // (現状はエラーがキャッチされておらずサーバーのログに流れて邪魔なので)
- const [user, miapp] = await this.authenticateService.authenticate(q.i as string);
+ try {
+ [user, app] = await this.authenticateService.authenticate(q.get('i'));
+ } catch (e) {
+ if (e instanceof AuthenticationError) {
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
+ } else {
+ socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
+ }
+ socket.destroy();
+ return;
+ }
if (user?.isSuspended) {
- request.reject(400);
+ socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
+ socket.destroy();
return;
}
+ const stream = new MainStreamConnection(
+ this.channelsService,
+ this.noteReadService,
+ this.notificationService,
+ this.cacheService,
+ user, app,
+ );
+
+ await stream.init();
+
+ this.#wss.handleUpgrade(request, socket, head, (ws) => {
+ this.#wss.emit('connection', ws, request, {
+ stream, user, app,
+ });
+ });
+ });
+
+ this.#wss.on('connection', async (connection: WebSocket.WebSocket, request: http.IncomingMessage, ctx: {
+ stream: MainStreamConnection,
+ user: LocalUser | null;
+ app: AccessToken | null
+ }) => {
+ const { stream, user, app } = ctx;
+
const ev = new EventEmitter();
async function onRedisMessage(_: string, data: string): Promise<void> {
@@ -83,21 +109,11 @@ export class StreamingApiServerService {
this.redisForSub.on('message', onRedisMessage);
- const main = new MainStreamConnection(
- this.channelsService,
- this.noteReadService,
- this.notificationService,
- this.cacheService,
- ev, user, miapp,
- );
+ await stream.listen(ev, connection);
- await main.init();
+ this.#connections.set(connection, Date.now());
- const connection = request.accept();
-
- main.init2(connection);
-
- const intervalId = user ? setInterval(() => {
+ const userUpdateIntervalId = user ? setInterval(() => {
this.usersRepository.update(user.id, {
lastActiveDate: new Date(),
});
@@ -110,16 +126,38 @@ export class StreamingApiServerService {
connection.once('close', () => {
ev.removeAllListeners();
- main.dispose();
+ stream.dispose();
this.redisForSub.off('message', onRedisMessage);
- if (intervalId) clearInterval(intervalId);
+ if (userUpdateIntervalId) clearInterval(userUpdateIntervalId);
});
connection.on('message', async (data) => {
- if (data.type === 'utf8' && data.utf8Data === 'ping') {
+ this.#connections.set(connection, Date.now());
+ if (data.toString() === 'ping') {
connection.send('pong');
}
});
});
+
+ this.#cleanConnectionsIntervalId = setInterval(() => {
+ const now = Date.now();
+ for (const [connection, lastActive] of this.#connections.entries()) {
+ if (now - lastActive > 1000 * 60 * 5) {
+ connection.terminate();
+ this.#connections.delete(connection);
+ }
+ }
+ }, 1000 * 60 * 5);
+ }
+
+ @bindThis
+ public detach(): Promise<void> {
+ if (this.#cleanConnectionsIntervalId) {
+ clearInterval(this.#cleanConnectionsIntervalId);
+ this.#cleanConnectionsIntervalId = null;
+ }
+ return new Promise((resolve) => {
+ this.#wss.close(() => resolve());
+ });
}
}
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 09bd7cbff4..7e678a6404 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -320,6 +320,9 @@ import * as ep___users_lists_list from './endpoints/users/lists/list.js';
import * as ep___users_lists_pull from './endpoints/users/lists/pull.js';
import * as ep___users_lists_push from './endpoints/users/lists/push.js';
import * as ep___users_lists_show from './endpoints/users/lists/show.js';
+import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js';
+import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
+import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js';
import * as ep___users_lists_update from './endpoints/users/lists/update.js';
import * as ep___users_notes from './endpoints/users/notes.js';
import * as ep___users_pages from './endpoints/users/pages.js';
@@ -656,7 +659,10 @@ const eps = [
['users/lists/pull', ep___users_lists_pull],
['users/lists/push', ep___users_lists_push],
['users/lists/show', ep___users_lists_show],
+ ['users/lists/favorite', ep___users_lists_favorite],
+ ['users/lists/unfavorite', ep___users_lists_unfavorite],
['users/lists/update', ep___users_lists_update],
+ ['users/lists/create-from-public', ep___users_lists_create_from_public],
['users/notes', ep___users_notes],
['users/pages', ep___users_pages],
['users/reactions', ep___users_reactions],
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts
index 2393c2441c..12db1f78fb 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts
@@ -25,7 +25,7 @@ export const paramDef = {
id: { type: 'string', format: 'misskey:id' },
title: { type: 'string', minLength: 1 },
text: { type: 'string', minLength: 1 },
- imageUrl: { type: 'string', nullable: true, minLength: 1 },
+ imageUrl: { type: 'string', nullable: true, minLength: 0 },
},
required: ['id', 'title', 'text', 'imageUrl'],
} as const;
@@ -46,7 +46,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
updatedAt: new Date(),
title: ps.title,
text: ps.text,
- imageUrl: ps.imageUrl,
+ /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */
+ imageUrl: ps.imageUrl || null,
});
});
}
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 2fb3e489e7..509224e9c3 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
@@ -25,9 +25,24 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
+ name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
fileId: { type: 'string', format: 'misskey:id' },
+ category: {
+ type: 'string',
+ nullable: true,
+ description: 'Use `null` to reset the category.',
+ },
+ aliases: { type: 'array', items: {
+ type: 'string',
+ } },
+ license: { type: 'string', nullable: true },
+ isSensitive: { type: 'boolean' },
+ localOnly: { type: 'boolean' },
+ roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
+ type: 'string',
+ } },
},
- required: ['fileId'],
+ required: ['name', 'fileId'],
} as const;
// TODO: ロジックをサービスに切り出す
@@ -45,18 +60,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) {
super(meta, paramDef, async (ps, me) => {
const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
-
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
- const name = driveFile.name.split('.')[0].match(/^[a-z0-9_]+$/) ? driveFile.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`;
-
const emoji = await this.customEmojiService.add({
driveFile,
- name,
- category: null,
- aliases: [],
+ name: ps.name,
+ category: ps.category ?? null,
+ aliases: ps.aliases ?? [],
host: null,
- license: null,
+ license: ps.license ?? null,
+ isSensitive: ps.isSensitive ?? false,
+ localOnly: ps.localOnly ?? false,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [],
});
this.moderationLogService.insertModerationLog(me, 'addEmoji', {
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 f63348b60b..fb22bdc477 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -1,6 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import type { DriveFilesRepository } from '@/models/index.js';
+import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -15,6 +17,11 @@ export const meta = {
code: 'NO_SUCH_EMOJI',
id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8',
},
+ noSuchFile: {
+ message: 'No such file.',
+ code: 'NO_SUCH_FILE',
+ id: '14fb9fd9-0731-4e2f-aeb9-f09e4740333d',
+ },
sameNameEmojiExists: {
message: 'Emoji that have same name already exists.',
code: 'SAME_NAME_EMOJI_EXISTS',
@@ -28,6 +35,7 @@ export const paramDef = {
properties: {
id: { type: 'string', format: 'misskey:id' },
name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
+ fileId: { type: 'string', format: 'misskey:id' },
category: {
type: 'string',
nullable: true,
@@ -37,6 +45,11 @@ export const paramDef = {
type: 'string',
} },
license: { type: 'string', nullable: true },
+ isSensitive: { type: 'boolean' },
+ localOnly: { type: 'boolean' },
+ roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
+ type: 'string',
+ } },
},
required: ['id', 'name', 'aliases'],
} as const;
@@ -45,14 +58,28 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
+ @Inject(DI.driveFilesRepository)
+ private driveFilesRepository: DriveFilesRepository,
+
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
+ let driveFile;
+
+ if (ps.fileId) {
+ driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
+ if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
+ }
+
await this.customEmojiService.update(ps.id, {
+ driveFile,
name: ps.name,
category: ps.category ?? null,
aliases: ps.aliases,
license: ps.license ?? null,
+ isSensitive: ps.isSensitive,
+ localOnly: ps.localOnly,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
});
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts
index f12738bd3a..f2d4aa8996 100644
--- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts
@@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) {
super(meta, paramDef, async (ps, me) => {
try {
- if (new URL(ps.inbox).protocol !== 'https:') throw 'https only';
+ if (new URL(ps.inbox).protocol !== 'https:') throw new Error('https only');
} catch {
throw new ApiError(meta.errors.invalidUrl);
}
diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts
index dca0f443b7..e756a9b510 100644
--- a/packages/backend/src/server/api/endpoints/antennas/notes.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts
@@ -113,6 +113,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
this.antennasRepository.update(antenna.id, {
+ isActive: true,
lastUsedAt: new Date(),
});
diff --git a/packages/backend/src/server/api/endpoints/auth/accept.ts b/packages/backend/src/server/api/endpoints/auth/accept.ts
index cb2e661bfb..05842460cf 100644
--- a/packages/backend/src/server/api/endpoints/auth/accept.ts
+++ b/packages/backend/src/server/api/endpoints/auth/accept.ts
@@ -55,7 +55,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchSession);
}
- // Generate access token
const accessToken = secureRndstr(32, true);
// Fetch exist access token
@@ -65,7 +64,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
});
if (exist == null) {
- // Lookup app
const app = await this.appsRepository.findOneByOrFail({ id: session.appId });
// Generate Hash
@@ -75,7 +73,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const now = new Date();
- // Insert access token doc
await this.accessTokensRepository.insert({
id: this.idService.genId(),
createdAt: now,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
index ad33398da6..e8985a9cd8 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
@@ -1,6 +1,6 @@
import { promisify } from 'node:util';
import bcrypt from 'bcryptjs';
-import * as cbor from 'cbor';
+import cbor from 'cbor';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts
index 3361e5a4d3..48fb03a8af 100644
--- a/packages/backend/src/server/api/endpoints/i/apps.ts
+++ b/packages/backend/src/server/api/endpoints/i/apps.ts
@@ -26,7 +26,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) {
super(meta, paramDef, async (ps, me) => {
const query = this.accessTokensRepository.createQueryBuilder('token')
- .where('token.userId = :userId', { userId: me.id });
+ .where('token.userId = :userId', { userId: me.id })
+ .leftJoinAndSelect('token.app', 'app');
switch (ps.sort) {
case '+createdAt': query.orderBy('token.createdAt', 'DESC'); break;
@@ -40,7 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
return await Promise.all(tokens.map(token => ({
id: token.id,
- name: token.name,
+ name: token.name ?? token.app?.name,
createdAt: token.createdAt,
lastUsedAt: token.lastUsedAt,
permission: token.permission,
diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts
index e141be764a..f5662f4a0e 100644
--- a/packages/backend/src/server/api/endpoints/i/notifications.ts
+++ b/packages/backend/src/server/api/endpoints/i/notifications.ts
@@ -91,18 +91,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
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][];
- const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
+ const limit = ps.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) as Notification[];
+ let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as Notification[];
if (includeTypes && includeTypes.length > 0) {
notifications = notifications.filter(notification => includeTypes.includes(notification.type));
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 74be00a8b8..8f5e6177c2 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -141,13 +141,12 @@ export const paramDef = {
preventAiLearning: { type: 'boolean' },
isBot: { type: 'boolean' },
isCat: { type: 'boolean' },
- showTimelineReplies: { type: 'boolean' },
injectFeaturedNote: { type: 'boolean' },
receiveAnnouncementEmail: { type: 'boolean' },
alwaysMarkNsfw: { type: 'boolean' },
autoSensitive: { type: 'boolean' },
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
- pinnedPageId: { type: 'string', format: 'misskey:id' },
+ pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
mutedWords: { type: 'array' },
mutedInstances: { type: 'array', items: {
type: 'string',
@@ -239,7 +238,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus;
if (typeof ps.publicReactions === 'boolean') profileUpdates.publicReactions = ps.publicReactions;
if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
- if (typeof ps.showTimelineReplies === 'boolean') updates.showTimelineReplies = ps.showTimelineReplies;
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts
index 584ea07c3b..53d724a9dd 100644
--- a/packages/backend/src/server/api/endpoints/meta.ts
+++ b/packages/backend/src/server/api/endpoints/meta.ts
@@ -1,5 +1,6 @@
import { IsNull, LessThanOrEqual, MoreThan } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
+import * as JSON5 from 'json5';
import type { AdsRepository, UsersRepository } from '@/models/index.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -292,8 +293,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
- defaultLightTheme: instance.defaultLightTheme,
- defaultDarkTheme: instance.defaultDarkTheme,
+ // クライアントの手間を減らすためあらかじめJSONに変換しておく
+ defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null,
+ defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null,
ads: ads.map(ad => ({
id: ad.id,
url: ad.url,
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index 3f7f2cdece..96be5ed844 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -99,7 +99,7 @@ export const paramDef = {
} },
cw: { type: 'string', nullable: true, maxLength: 100 },
localOnly: { type: 'boolean', default: false },
- reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote'], default: null },
+ reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
noExtractMentions: { type: 'boolean', default: false },
noExtractHashtags: { type: 'boolean', default: false },
noExtractEmojis: { type: 'boolean', default: false },
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 c11c1eac40..88c1ca7f58 100644
--- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
@@ -34,11 +34,8 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
- withFiles: {
- type: 'boolean',
- default: false,
- description: 'Only show notes that have attached files.',
- },
+ withFiles: { type: 'boolean', default: false },
+ withReplies: { type: 'boolean', default: false },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
@@ -78,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
- this.queryService.generateRepliesQuery(query, me);
+ this.queryService.generateRepliesQuery(query, ps.withReplies, me);
if (me) {
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(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 89abd91c7e..7a3581e6e4 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -46,11 +46,8 @@ export const paramDef = {
includeMyRenotes: { type: 'boolean', default: true },
includeRenotedMyNotes: { type: 'boolean', default: true },
includeLocalRenotes: { type: 'boolean', default: true },
- withFiles: {
- type: 'boolean',
- default: false,
- description: 'Only show notes that have attached files.',
- },
+ withFiles: { type: 'boolean', default: false },
+ withReplies: { type: 'boolean', default: false },
},
required: [],
} as const;
@@ -98,7 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.setParameters(followingQuery.getParameters());
this.queryService.generateChannelQuery(query, me);
- this.queryService.generateRepliesQuery(query, me);
+ this.queryService.generateRepliesQuery(query, ps.withReplies, me);
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);
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 afdafc7c55..2ee549232c 100644
--- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -36,11 +36,8 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
- withFiles: {
- type: 'boolean',
- default: false,
- description: 'Only show notes that have attached files.',
- },
+ withFiles: { type: 'boolean', default: false },
+ withReplies: { type: 'boolean', default: false },
fileType: { type: 'array', items: {
type: 'string',
} },
@@ -86,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateChannelQuery(query, me);
- this.queryService.generateRepliesQuery(query, me);
+ this.queryService.generateRepliesQuery(query, ps.withReplies, me);
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateMutedNoteQuery(query, me);
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 2956bf1cbd..742df0ca95 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
@@ -82,14 +82,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
try {
if (ps.tag) {
- if (!safeForSql(normalizeForSearch(ps.tag))) throw 'Injection';
+ if (!safeForSql(normalizeForSearch(ps.tag))) throw new Error('Injection');
query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
} else {
query.andWhere(new Brackets(qb => {
for (const tags of ps.query!) {
qb.orWhere(new Brackets(qb => {
for (const tag of tags) {
- if (!safeForSql(normalizeForSearch(tag))) throw 'Injection';
+ if (!safeForSql(normalizeForSearch(tag))) throw new Error('Injection');
qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`);
}
}));
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
index c6ee1e5c2b..e1f286439b 100644
--- a/packages/backend/src/server/api/endpoints/notes/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts
@@ -35,11 +35,8 @@ export const paramDef = {
includeMyRenotes: { type: 'boolean', default: true },
includeRenotedMyNotes: { type: 'boolean', default: true },
includeLocalRenotes: { type: 'boolean', default: true },
- withFiles: {
- type: 'boolean',
- default: false,
- description: 'Only show notes that have attached files.',
- },
+ withFiles: { type: 'boolean', default: false },
+ withReplies: { type: 'boolean', default: false },
},
required: [],
} as const;
@@ -84,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
this.queryService.generateChannelQuery(query, me);
- this.queryService.generateRepliesQuery(query, me);
+ this.queryService.generateRepliesQuery(query, ps.withReplies, me);
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);
diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts
index 4ced6d3ff1..1d4825f812 100644
--- a/packages/backend/src/server/api/endpoints/reset-db.ts
+++ b/packages/backend/src/server/api/endpoints/reset-db.ts
@@ -34,7 +34,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private redisClient: Redis.Redis,
) {
super(meta, paramDef, async (ps, me) => {
- if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
+ if (process.env.NODE_ENV !== 'test') throw new Error('NODE_ENV is not a test');
await redisClient.flushdb();
await resetDb(this.db);
diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts
index 6202c740f1..42e36cb04a 100644
--- a/packages/backend/src/server/api/endpoints/roles/notes.ts
+++ b/packages/backend/src/server/api/endpoints/roles/notes.ts
@@ -93,6 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
+ .andWhere('(note.visibility = \'public\')')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
diff --git a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
new file mode 100644
index 0000000000..8591e4ab96
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
@@ -0,0 +1,148 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/index.js';
+import { IdService } from '@/core/IdService.js';
+import type { UserList } from '@/models/entities/UserList.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { GetterService } from '@/server/api/GetterService.js';
+import { UserListEntityService } from '@/core/entities/UserListEntityService.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { RoleService } from '@/core/RoleService.js';
+import { UserListService } from '@/core/UserListService.js';
+
+export const meta = {
+ requireCredential: true,
+ prohibitMoved: true,
+ res: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'UserList',
+ },
+
+ errors: {
+ tooManyUserLists: {
+ message: 'You cannot create user list any more.',
+ code: 'TOO_MANY_USERLISTS',
+ id: 'e9c105b2-c595-47de-97fb-7f7c2c33e92f',
+ },
+ noSuchList: {
+ message: 'No such list.',
+ code: 'NO_SUCH_LIST',
+ id: '9292f798-6175-4f7d-93f4-b6742279667d',
+ },
+ noSuchUser: {
+ message: 'No such user.',
+ code: 'NO_SUCH_USER',
+ id: '13c457db-a8cb-4d88-b70a-211ceeeabb5f',
+ },
+
+ alreadyAdded: {
+ message: 'That user has already been added to that list.',
+ code: 'ALREADY_ADDED',
+ id: 'c3ad6fdb-692b-47ee-a455-7bd12c7af615',
+ },
+
+ youHaveBeenBlocked: {
+ message: 'You cannot push this user because you have been blocked by this user.',
+ code: 'YOU_HAVE_BEEN_BLOCKED',
+ id: 'a2497f2a-2389-439c-8626-5298540530f4',
+ },
+
+ tooManyUsers: {
+ message: 'You can not push users any more.',
+ code: 'TOO_MANY_USERS',
+ id: '1845ea77-38d1-426e-8e4e-8b83b24f5bd7',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ name: { type: 'string', minLength: 1, maxLength: 100 },
+ listId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['name', 'listId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.userListsRepository)
+ private userListsRepository: UserListsRepository,
+
+ @Inject(DI.userListJoiningsRepository)
+ private userListJoiningsRepository: UserListJoiningsRepository,
+
+ @Inject(DI.blockingsRepository)
+ private blockingsRepository: BlockingsRepository,
+
+ private userListService: UserListService,
+ private userListEntityService: UserListEntityService,
+ private idService: IdService,
+ private getterService: GetterService,
+ private roleService: RoleService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const list = await this.userListsRepository.findOneBy({
+ id: ps.listId,
+ isPublic: true,
+ });
+ if (list === null) throw new ApiError(meta.errors.noSuchList);
+ const currentCount = await this.userListsRepository.countBy({
+ userId: me.id,
+ });
+ if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) {
+ throw new ApiError(meta.errors.tooManyUserLists);
+ }
+
+ const userList = await this.userListsRepository.insert({
+ id: this.idService.genId(),
+ createdAt: new Date(),
+ userId: me.id,
+ name: ps.name,
+ } as UserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
+
+ const users = (await this.userListJoiningsRepository.findBy({
+ userListId: ps.listId,
+ })).map(x => x.userId);
+
+ for (const user of users) {
+ const currentUser = await this.getterService.getUser(user).catch(err => {
+ if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+ throw err;
+ });
+
+ if (currentUser.id !== me.id) {
+ const block = await this.blockingsRepository.findOneBy({
+ blockerId: currentUser.id,
+ blockeeId: me.id,
+ });
+ if (block) {
+ throw new ApiError(meta.errors.youHaveBeenBlocked);
+ }
+ }
+
+ const exist = await this.userListJoiningsRepository.findOneBy({
+ userListId: userList.id,
+ userId: currentUser.id,
+ });
+
+ if (exist) {
+ throw new ApiError(meta.errors.alreadyAdded);
+ }
+
+ try {
+ await this.userListService.push(currentUser, userList, me);
+ } catch (err) {
+ if (err instanceof UserListService.TooManyUsersError) {
+ throw new ApiError(meta.errors.tooManyUsers);
+ }
+ throw err;
+ }
+ }
+ return await this.userListEntityService.pack(userList);
+ });
+ }
+}
+
diff --git a/packages/backend/src/server/api/endpoints/users/lists/favorite.ts b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts
new file mode 100644
index 0000000000..263852fde1
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts
@@ -0,0 +1,70 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { UserListFavoritesRepository, UserListsRepository } from '@/models/index.js';
+import { IdService } from '@/core/IdService.js';
+import { ApiError } from '@/server/api/error.js';
+import { DI } from '@/di-symbols.js';
+
+export const meta = {
+ requireCredential: true,
+ errors: {
+ noSuchList: {
+ message: 'No such user list.',
+ code: 'NO_SUCH_USER_LIST',
+ id: '7dbaf3cf-7b42-4b8f-b431-b3919e580dbe',
+ },
+
+ alreadyFavorited: {
+ message: 'The list has already been favorited.',
+ code: 'ALREADY_FAVORITED',
+ id: '6425bba0-985b-461e-af1b-518070e72081',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ listId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['listId'],
+} as const;
+
+@Injectable() // eslint-disable-next-line import/no-default-export
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor (
+ @Inject(DI.userListsRepository)
+ private userListsRepository: UserListsRepository,
+
+ @Inject(DI.userListFavoritesRepository)
+ private userListFavoritesRepository: UserListFavoritesRepository,
+ private idService: IdService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const userList = await this.userListsRepository.findOneBy({
+ id: ps.listId,
+ isPublic: true,
+ });
+
+ if (userList === null) {
+ throw new ApiError(meta.errors.noSuchList);
+ }
+
+ const exist = await this.userListFavoritesRepository.findOneBy({
+ userId: me.id,
+ userListId: ps.listId,
+ });
+
+ if (exist !== null) {
+ throw new ApiError(meta.errors.alreadyFavorited);
+ }
+
+ await this.userListFavoritesRepository.insert({
+ id: this.idService.genId(),
+ createdAt: new Date(),
+ userId: me.id,
+ userListId: ps.listId,
+ });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/users/lists/list.ts b/packages/backend/src/server/api/endpoints/users/lists/list.ts
index 2104c4377d..eab29944b2 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/list.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/list.ts
@@ -1,13 +1,14 @@
import { Inject, Injectable } from '@nestjs/common';
-import type { UserListsRepository } from '@/models/index.js';
+import type { UserListsRepository, UsersRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserListEntityService } from '@/core/entities/UserListEntityService.js';
+import { ApiError } from '@/server/api/error.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['lists', 'account'],
- requireCredential: true,
+ requireCredential: false,
kind: 'read:account',
@@ -22,26 +23,58 @@ export const meta = {
ref: 'UserList',
},
},
+ errors: {
+ noSuchUser: {
+ message: 'No such user.',
+ code: 'NO_SUCH_USER',
+ id: 'a8af4a82-0980-4cc4-a6af-8b0ffd54465e',
+ },
+ remoteUser: {
+ message: 'Not allowed to load the remote user\'s list',
+ code: 'REMOTE_USER_NOT_ALLOWED',
+ id: '53858f1b-3315-4a01-81b7-db9b48d4b79a',
+ },
+ invalidParam: {
+ message: 'Invalid param.',
+ code: 'INVALID_PARAM',
+ id: 'ab36de0e-29e9-48cb-9732-d82f1281620d',
+ },
+ },
} as const;
export const paramDef = {
type: 'object',
- properties: {},
+ properties: {
+ userId: { type: 'string', format: 'misskey:id' },
+ },
required: [],
} as const;
-// eslint-disable-next-line import/no-default-export
-@Injectable()
+@Injectable() // eslint-disable-next-line import/no-default-export
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+
@Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,
private userListEntityService: UserListEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- const userLists = await this.userListsRepository.findBy({
+ if (typeof ps.userId !== 'undefined') {
+ const user = await this.usersRepository.findOneBy({ id: ps.userId });
+ if (user === null) throw new ApiError(meta.errors.noSuchUser);
+ if (user.host !== null) throw new ApiError(meta.errors.remoteUser);
+ } else if (me === null) {
+ throw new ApiError(meta.errors.invalidParam);
+ }
+
+ const userLists = await this.userListsRepository.findBy(typeof ps.userId === 'undefined' && me !== null ? {
userId: me.id,
+ } : {
+ userId: ps.userId,
+ isPublic: true,
});
return await Promise.all(userLists.map(x => this.userListEntityService.pack(x)));
diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts
index 77f9cba808..8077841c8c 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/show.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
-import type { UserListsRepository } from '@/models/index.js';
+import type { UserListsRepository, UserListFavoritesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserListEntityService } from '@/core/entities/UserListEntityService.js';
import { DI } from '@/di-symbols.js';
@@ -8,7 +8,7 @@ import { ApiError } from '../../../error.js';
export const meta = {
tags: ['lists', 'account'],
- requireCredential: true,
+ requireCredential: false,
kind: 'read:account',
@@ -33,31 +33,54 @@ export const paramDef = {
type: 'object',
properties: {
listId: { type: 'string', format: 'misskey:id' },
+ forPublic: { type: 'boolean', default: false },
},
required: ['listId'],
} as const;
-// eslint-disable-next-line import/no-default-export
-@Injectable()
+@Injectable() // eslint-disable-next-line import/no-default-export
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,
+ @Inject(DI.userListFavoritesRepository)
+ private userListFavoritesRepository: UserListFavoritesRepository,
+
private userListEntityService: UserListEntityService,
) {
super(meta, paramDef, async (ps, me) => {
+ const additionalProperties: Partial<{ likedCount: number, isLiked: boolean }> = {};
// Fetch the list
- const userList = await this.userListsRepository.findOneBy({
+ const userList = await this.userListsRepository.findOneBy(!ps.forPublic && me !== null ? {
id: ps.listId,
userId: me.id,
+ } : {
+ id: ps.listId,
+ isPublic: true,
});
if (userList == null) {
throw new ApiError(meta.errors.noSuchList);
}
- return await this.userListEntityService.pack(userList);
+ if (ps.forPublic && userList.isPublic) {
+ additionalProperties.likedCount = await this.userListFavoritesRepository.countBy({
+ userListId: ps.listId,
+ });
+ if (me !== null) {
+ additionalProperties.isLiked = (await this.userListFavoritesRepository.findOneBy({
+ userId: me.id,
+ userListId: ps.listId,
+ }) !== null);
+ } else {
+ additionalProperties.isLiked = false;
+ }
+ }
+ return {
+ ...await this.userListEntityService.pack(userList),
+ ...additionalProperties,
+ };
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts
new file mode 100644
index 0000000000..be8e317816
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts
@@ -0,0 +1,63 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { UserListFavoritesRepository, UserListsRepository } from '@/models/index.js';
+import { ApiError } from '@/server/api/error.js';
+import { DI } from '@/di-symbols.js';
+
+export const meta = {
+ requireCredential: true,
+ errors: {
+ noSuchList: {
+ message: 'No such user list.',
+ code: 'NO_SUCH_USER_LIST',
+ id: 'baedb33e-76b8-4b0c-86a8-9375c0a7b94b',
+ },
+
+ notFavorited: {
+ message: 'You have not favorited the list.',
+ code: 'ALREADY_FAVORITED',
+ id: '835c4b27-463d-4cfa-969b-a9058678d465',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ listId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['listId'],
+} as const;
+
+@Injectable() // eslint-disable-next-line import/no-default-export
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor (
+ @Inject(DI.userListsRepository)
+ private userListsRepository: UserListsRepository,
+
+ @Inject(DI.userListFavoritesRepository)
+ private userListFavoritesRepository: UserListFavoritesRepository,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const userList = await this.userListsRepository.findOneBy({
+ id: ps.listId,
+ isPublic: true,
+ });
+
+ if (userList === null) {
+ throw new ApiError(meta.errors.noSuchList);
+ }
+
+ const exist = await this.userListFavoritesRepository.findOneBy({
+ userListId: ps.listId,
+ userId: me.id,
+ });
+
+ if (exist === null) {
+ throw new ApiError(meta.errors.notFavorited);
+ }
+
+ await this.userListFavoritesRepository.delete({ id: exist.id });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts
index 6453d7d980..b0a95a2f28 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/update.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts
@@ -34,8 +34,9 @@ export const paramDef = {
properties: {
listId: { type: 'string', format: 'misskey:id' },
name: { type: 'string', minLength: 1, maxLength: 100 },
+ isPublic: { type: 'boolean' },
},
- required: ['listId', 'name'],
+ required: ['listId'],
} as const;
// eslint-disable-next-line import/no-default-export
@@ -48,7 +49,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private userListEntityService: UserListEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- // Fetch the list
const userList = await this.userListsRepository.findOneBy({
id: ps.listId,
userId: me.id,
@@ -60,6 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
await this.userListsRepository.update(userList.id, {
name: ps.name,
+ isPublic: ps.isPublic,
});
return await this.userListEntityService.pack(userList.id);
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 5454836fe1..d3339072c1 100644
--- a/packages/backend/src/server/api/stream/channels/global-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -13,6 +13,7 @@ class GlobalTimelineChannel extends Channel {
public readonly chName = 'globalTimeline';
public static shouldShare = true;
public static requireCredential = false;
+ private withReplies: boolean;
constructor(
private metaService: MetaService,
@@ -31,6 +32,8 @@ class GlobalTimelineChannel extends Channel {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.gtlAvailable) return;
+ this.withReplies = params.withReplies as boolean;
+
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
}
@@ -54,7 +57,7 @@ class GlobalTimelineChannel extends Channel {
}
// 関係ない返信は除外
- if (note.reply && !this.user!.showTimelineReplies) {
+ if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts
index ee874ad81e..1755aa94cf 100644
--- a/packages/backend/src/server/api/stream/channels/home-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts
@@ -11,6 +11,7 @@ class HomeTimelineChannel extends Channel {
public readonly chName = 'homeTimeline';
public static shouldShare = true;
public static requireCredential = true;
+ private withReplies: boolean;
constructor(
private noteEntityService: NoteEntityService,
@@ -24,6 +25,8 @@ class HomeTimelineChannel extends Channel {
@bindThis
public async init(params: any) {
+ this.withReplies = params.withReplies as boolean;
+
this.subscriber.on('notesStream', this.onNote);
}
@@ -63,7 +66,7 @@ class HomeTimelineChannel extends Channel {
}
// 関係ない返信は除外
- if (note.reply && !this.user!.showTimelineReplies) {
+ if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
index 4f7b4e78b6..5a33e13cf5 100644
--- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
@@ -13,6 +13,7 @@ class HybridTimelineChannel extends Channel {
public readonly chName = 'hybridTimeline';
public static shouldShare = true;
public static requireCredential = true;
+ private withReplies: boolean;
constructor(
private metaService: MetaService,
@@ -31,6 +32,8 @@ class HybridTimelineChannel extends Channel {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return;
+ this.withReplies = params.withReplies as boolean;
+
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
}
@@ -75,7 +78,7 @@ class HybridTimelineChannel extends Channel {
if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances ?? []))) return;
// 関係ない返信は除外
- if (note.reply && !this.user!.showTimelineReplies) {
+ if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) 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 09b0005ac1..9ca4db8ced 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -12,6 +12,7 @@ class LocalTimelineChannel extends Channel {
public readonly chName = 'localTimeline';
public static shouldShare = true;
public static requireCredential = false;
+ private withReplies: boolean;
constructor(
private metaService: MetaService,
@@ -30,6 +31,8 @@ class LocalTimelineChannel extends Channel {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return;
+ this.withReplies = params.withReplies as boolean;
+
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
}
@@ -54,7 +57,7 @@ class LocalTimelineChannel extends Channel {
}
// 関係ない返信は除外
- if (note.reply && this.user && !this.user.showTimelineReplies) {
+ if (note.reply && this.user && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts
index 9d106c8b2f..ab9c1aa0b5 100644
--- a/packages/backend/src/server/api/stream/channels/role-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts
@@ -5,15 +5,17 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import Channel from '../channel.js';
import { StreamMessages } from '../types.js';
+import { RoleService } from '@/core/RoleService.js';
class RoleTimelineChannel extends Channel {
public readonly chName = 'roleTimeline';
public static shouldShare = false;
public static requireCredential = false;
private roleId: string;
-
+
constructor(
private noteEntityService: NoteEntityService,
+ private roleservice: RoleService,
id: string,
connection: Channel['connection'],
@@ -34,6 +36,11 @@ class RoleTimelineChannel extends Channel {
if (data.type === 'note') {
const note = data.body;
+ if (!(await this.roleservice.isExplorable({ id: this.roleId }))) {
+ return;
+ }
+ if (note.visibility !== 'public') return;
+
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
@@ -61,6 +68,7 @@ export class RoleTimelineChannelService {
constructor(
private noteEntityService: NoteEntityService,
+ private roleservice: RoleService,
) {
}
@@ -68,6 +76,7 @@ export class RoleTimelineChannelService {
public create(id: string, connection: Channel['connection']): RoleTimelineChannel {
return new RoleTimelineChannel(
this.noteEntityService,
+ this.roleservice,
id,
connection,
);
diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts
index a6f9145952..8b1c2c09c9 100644
--- a/packages/backend/src/server/api/stream/index.ts
+++ b/packages/backend/src/server/api/stream/index.ts
@@ -1,3 +1,4 @@
+import * as WebSocket from 'ws';
import type { User } from '@/models/entities/User.js';
import type { AccessToken } from '@/models/entities/AccessToken.js';
import type { Packed } from '@/misc/json-schema.js';
@@ -7,7 +8,6 @@ import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { UserProfile } from '@/models/index.js';
import type { ChannelsService } from './ChannelsService.js';
-import type * as websocket from 'websocket';
import type { EventEmitter } from 'events';
import type Channel from './channel.js';
import type { StreamEventEmitter, StreamMessages } from './types.js';
@@ -18,7 +18,7 @@ import type { StreamEventEmitter, StreamMessages } from './types.js';
export default class Connection {
public user?: User;
public token?: AccessToken;
- private wsConnection: websocket.connection;
+ private wsConnection: WebSocket.WebSocket;
public subscriber: StreamEventEmitter;
private channels: Channel[] = [];
private subscribingNotes: any = {};
@@ -37,11 +37,9 @@ export default class Connection {
private notificationService: NotificationService,
private cacheService: CacheService,
- subscriber: EventEmitter,
user: User | null | undefined,
token: AccessToken | null | undefined,
) {
- this.subscriber = subscriber;
if (user) this.user = user;
if (token) this.token = token;
}
@@ -70,12 +68,16 @@ export default class Connection {
if (this.user != null) {
await this.fetch();
- this.fetchIntervalId = setInterval(this.fetch, 1000 * 10);
+ if (!this.fetchIntervalId) {
+ this.fetchIntervalId = setInterval(this.fetch, 1000 * 10);
+ }
}
}
@bindThis
- public async init2(wsConnection: websocket.connection) {
+ public async listen(subscriber: EventEmitter, wsConnection: WebSocket.WebSocket) {
+ this.subscriber = subscriber;
+
this.wsConnection = wsConnection;
this.wsConnection.on('message', this.onWsConnectionMessage);
@@ -88,14 +90,11 @@ export default class Connection {
* クライアントからメッセージ受信時
*/
@bindThis
- private async onWsConnectionMessage(data: websocket.Message) {
- if (data.type !== 'utf8') return;
- if (data.utf8Data == null) return;
-
+ private async onWsConnectionMessage(data: WebSocket.RawData) {
let obj: Record<string, any>;
try {
- obj = JSON.parse(data.utf8Data);
+ obj = JSON.parse(data.toString());
} catch (e) {
return;
}
@@ -246,7 +245,7 @@ export default class Connection {
const ch: Channel = channelService.create(id, this);
this.channels.push(ch);
- ch.init(params);
+ ch.init(params ?? {});
if (pong) {
this.sendMessageToWs('connected', {