From a96e5277db7f638d6601fb1d714d95d6d357ca35 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 19 Oct 2021 20:25:47 +0900 Subject: [PATCH 01/12] =?UTF-8?q?fix(api):=20=E3=82=A2=E3=83=97=E3=83=AA?= =?UTF-8?q?=E3=82=B1=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E3=81=8C=E5=8F=96=E5=BE=97=E3=81=A7=E3=81=8D=E3=81=AA=E3=81=84?= =?UTF-8?q?=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #6702 --- CHANGELOG.md | 1 + CONTRIBUTING.md | 3 +++ src/server/api/endpoints/i/notifications.ts | 11 +++++++++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a07096b3dc..705c093a11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ### Bugfixes - クライアント: テーマの管理が行えない問題を修正 +- API: アプリケーション通知が取得できない問題を修正 ## 12.92.0 (2021/10/16) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ec8baaacb..76267ab30e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -171,6 +171,9 @@ const users = userIds.length > 0 ? await Users.find({ SQLでは配列のインデックスは**1始まり**。 `[a, b, c]`の `a`にアクセスしたいなら`[0]`ではなく`[1]`と書く +### null IN +nullが含まれる可能性のあるカラムにINするときは、そのままだとおかしくなるのでORなどでnullのハンドリングをしよう。 + ### `undefined`にご用心 MongoDBの時とは違い、findOneでレコードを取得する時に対象レコードが存在しない場合 **`undefined`** が返ってくるので注意。 MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`とか書くとバグる。代わりに`if (x == null)`と書いてください diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts index fcabbbc3dd..56668d03b7 100644 --- a/src/server/api/endpoints/i/notifications.ts +++ b/src/server/api/endpoints/i/notifications.ts @@ -6,6 +6,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query'; import { Notifications, Followings, Mutings, Users } from '@/models/index'; import { notificationTypes } from '@/types'; import read from '@/services/note/read'; +import { Brackets } from 'typeorm'; export const meta = { tags: ['account', 'notifications'], @@ -94,10 +95,16 @@ export default define(meta, async (ps, user) => { .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - query.andWhere(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`); + query.andWhere(new Brackets(qb => { qb + .where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`) + .orWhere('notification.notifierId IS NULL'); + })); query.setParameters(mutingQuery.getParameters()); - query.andWhere(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`); + query.andWhere(new Brackets(qb => { qb + .where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`) + .orWhere('notification.notifierId IS NULL'); + })); if (ps.following) { query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: user.id }); From 0cc055de3a21205092c9d3c95a7c25426e7309bf Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 19 Oct 2021 20:35:29 +0900 Subject: [PATCH 02/12] :art: --- src/client/components/global/header.vue | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/client/components/global/header.vue b/src/client/components/global/header.vue index a4466da498..2bf490c98a 100644 --- a/src/client/components/global/header.vue +++ b/src/client/components/global/header.vue @@ -203,6 +203,12 @@ export default defineComponent({ &.thin { --height: 50px; + + > .buttons { + > .button { + font-size: 0.9em; + } + } } &.slim { From 2d196b6779e2c4a0594fdd512c6cd4caaba043b0 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 20 Oct 2021 00:53:38 +0900 Subject: [PATCH 03/12] Update search-by-username-and-host.ts --- .../users/search-by-username-and-host.ts | 69 ++++++++++++------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/src/server/api/endpoints/users/search-by-username-and-host.ts b/src/server/api/endpoints/users/search-by-username-and-host.ts index 3b8d024af5..f03a55449d 100644 --- a/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -1,8 +1,9 @@ import $ from 'cafy'; import define from '../../define'; -import { Users } from '@/models/index'; +import { Followings, Users } from '@/models/index'; import { Brackets } from 'typeorm'; import { USER_ACTIVE_THRESHOLD } from '@/const'; +import { User } from '@/models/entities/user'; export const meta = { tags: ['users'], @@ -18,11 +19,6 @@ export const meta = { validator: $.optional.nullable.str, }, - offset: { - validator: $.optional.num.min(0), - default: 0, - }, - limit: { validator: $.optional.num.range(1, 100), default: 10, @@ -60,34 +56,59 @@ export default define(meta, async (ps, me) => { q.andWhere('user.updatedAt IS NOT NULL'); q.orderBy('user.updatedAt', 'DESC'); - const users = await q.take(ps.limit!).skip(ps.offset).getMany(); + const users = await q.take(ps.limit!).getMany(); return await Users.packMany(users, me, { detail: ps.detail }); } else if (ps.username) { - let users = await Users.createQueryBuilder('user') - .where('user.host IS NULL') - .andWhere('user.isSuspended = FALSE') - .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); - })) - .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') - .take(ps.limit!) - .skip(ps.offset) - .getMany(); + let users: User[] = []; - if (users.length < ps.limit!) { - const otherUsers = await Users.createQueryBuilder('user') - .where('user.host IS NOT NULL') + if (me) { + const followingQuery = Followings.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }); + + const query = Users.createQueryBuilder('user') + .where(`user.id IN (${ followingQuery.getQuery() })`) + .andWhere(`user.id != :meId`, { meId: me.id }) .andWhere('user.isSuspended = FALSE') .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) + .andWhere(new Brackets(qb => { qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })); + + query.setParameters(followingQuery.getParameters()); + + users = await query + .orderBy('user.usernameLower', 'ASC') + .take(ps.limit!) + .getMany(); + + if (users.length < ps.limit!) { + const otherQuery = await Users.createQueryBuilder('user') + .where(`user.id NOT IN (${ followingQuery.getQuery() })`) + .andWhere(`user.id != :meId`, { meId: me.id }) + .andWhere('user.isSuspended = FALSE') + .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) + .andWhere('user.updatedAt IS NOT NULL'); + + otherQuery.setParameters(followingQuery.getParameters()); + + const otherUsers = await otherQuery + .orderBy('user.updatedAt', 'DESC') + .take(ps.limit! - users.length) + .getMany(); + + users = users.concat(otherUsers); + } + } else { + users = await Users.createQueryBuilder('user') + .where('user.isSuspended = FALSE') + .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) .andWhere('user.updatedAt IS NOT NULL') .orderBy('user.updatedAt', 'DESC') .take(ps.limit! - users.length) .getMany(); - - users = users.concat(otherUsers); } return await Users.packMany(users, me, { detail: ps.detail }); From 386d3cd99723b85b8c0a953faab261c4fe425bef Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 20 Oct 2021 00:55:38 +0900 Subject: [PATCH 04/12] fix bug #7874 --- src/client/components/mfm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/components/mfm.ts b/src/client/components/mfm.ts index 2bdd7d46ee..f3411cadc3 100644 --- a/src/client/components/mfm.ts +++ b/src/client/components/mfm.ts @@ -185,7 +185,7 @@ export default defineComponent({ } } if (style == null) { - return h('span', {}, ['[', token.props.name, ' ', ...genEl(token.children), ']']); + return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']']); } else { return h('span', { style: 'display: inline-block;' + style, From 0005de6a988d8761f040fa23cd8d1acddeb9883b Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 20 Oct 2021 01:02:04 +0900 Subject: [PATCH 05/12] refactor: use insert --- src/server/api/endpoints/mute/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts index 5163ed63db..3fc64d3eba 100644 --- a/src/server/api/endpoints/mute/create.ts +++ b/src/server/api/endpoints/mute/create.ts @@ -67,7 +67,7 @@ export default define(meta, async (ps, user) => { } // Create mute - await Mutings.save({ + await Mutings.insert({ id: genId(), createdAt: new Date(), muterId: muter.id, From bc19cd77adfc1ba73df5358dac0a7b49166b02ac Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 20 Oct 2021 01:11:13 +0900 Subject: [PATCH 06/12] =?UTF-8?q?feat:=20=E3=83=9F=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=81=A8=E3=83=96=E3=83=AD=E3=83=83=E3=82=AF=E3=81=AE?= =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve #7885 --- CHANGELOG.md | 1 + src/client/pages/settings/import-export.vue | 20 +++-- src/queue/index.ts | 20 +++++ src/queue/processors/db/import-blocking.ts | 74 +++++++++++++++++ src/queue/processors/db/import-muting.ts | 83 +++++++++++++++++++ src/queue/processors/db/index.ts | 4 + src/server/api/endpoints/i/import-blocking.ts | 60 ++++++++++++++ src/server/api/endpoints/i/import-muting.ts | 60 ++++++++++++++ 8 files changed, 314 insertions(+), 8 deletions(-) create mode 100644 src/queue/processors/db/import-blocking.ts create mode 100644 src/queue/processors/db/import-muting.ts create mode 100644 src/server/api/endpoints/i/import-blocking.ts create mode 100644 src/server/api/endpoints/i/import-muting.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 705c093a11..2a3d9a1caa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - クライアント: 新しいライトテーマを追加 - API: ユーザーのリアクション一覧を取得する users/reactions を追加 - API: users/search および users/search-by-username-and-host を強化 +- ミュート及びブロックのインポートを行えるように ### Bugfixes - クライアント: テーマの管理が行えない問題を修正 diff --git a/src/client/pages/settings/import-export.vue b/src/client/pages/settings/import-export.vue index 2b49996dda..eeaa1f1602 100644 --- a/src/client/pages/settings/import-export.vue +++ b/src/client/pages/settings/import-export.vue @@ -16,11 +16,13 @@ - {{ $ts.export }} + {{ $ts.export }} + {{ $ts.import }} {{ $ts.export }} + {{ $ts.import }} @@ -58,11 +60,11 @@ export default defineComponent({ methods: { doExport(target) { os.api( - target == 'notes' ? 'i/export-notes' : - target == 'following' ? 'i/export-following' : - target == 'blocking' ? 'i/export-blocking' : - target == 'user-lists' ? 'i/export-user-lists' : - target == 'mute' ? 'i/export-mute' : + target === 'notes' ? 'i/export-notes' : + target === 'following' ? 'i/export-following' : + target === 'blocking' ? 'i/export-blocking' : + target === 'user-lists' ? 'i/export-user-lists' : + target === 'muting' ? 'i/export-mute' : null, {}) .then(() => { os.dialog({ @@ -81,8 +83,10 @@ export default defineComponent({ const file = await selectFile(e.currentTarget || e.target); os.api( - target == 'following' ? 'i/import-following' : - target == 'user-lists' ? 'i/import-user-lists' : + target === 'following' ? 'i/import-following' : + target === 'user-lists' ? 'i/import-user-lists' : + target === 'muting' ? 'i/import-muting' : + target === 'blocking' ? 'i/import-blocking' : null, { fileId: file.id }).then(() => { diff --git a/src/queue/index.ts b/src/queue/index.ts index 1e1d5da5a2..43c062bae7 100644 --- a/src/queue/index.ts +++ b/src/queue/index.ts @@ -163,6 +163,26 @@ export function createImportFollowingJob(user: ThinUser, fileId: DriveFile['id'] }); } +export function createImportMutingJob(user: ThinUser, fileId: DriveFile['id']) { + return dbQueue.add('importMuting', { + user: user, + fileId: fileId + }, { + removeOnComplete: true, + removeOnFail: true + }); +} + +export function createImportBlockingJob(user: ThinUser, fileId: DriveFile['id']) { + return dbQueue.add('importBlocking', { + user: user, + fileId: fileId + }, { + removeOnComplete: true, + removeOnFail: true + }); +} + export function createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']) { return dbQueue.add('importUserLists', { user: user, diff --git a/src/queue/processors/db/import-blocking.ts b/src/queue/processors/db/import-blocking.ts new file mode 100644 index 0000000000..ab3b91fc0e --- /dev/null +++ b/src/queue/processors/db/import-blocking.ts @@ -0,0 +1,74 @@ +import * as Bull from 'bull'; + +import { queueLogger } from '../../logger'; +import { parseAcct } from '@/misc/acct'; +import { resolveUser } from '@/remote/resolve-user'; +import { downloadTextFile } from '@/misc/download-text-file'; +import { isSelfHost, toPuny } from '@/misc/convert-host'; +import { Users, DriveFiles, Blockings } from '@/models/index'; +import { DbUserImportJobData } from '@/queue/types'; +import block from '@/services/blocking/create'; + +const logger = queueLogger.createSubLogger('import-blocking'); + +export async function importBlocking(job: Bull.Job, done: any): Promise { + logger.info(`Importing blocking of ${job.data.user.id} ...`); + + const user = await Users.findOne(job.data.user.id); + if (user == null) { + done(); + return; + } + + const file = await DriveFiles.findOne({ + id: job.data.fileId + }); + if (file == null) { + done(); + return; + } + + const csv = await downloadTextFile(file.url); + + let linenum = 0; + + for (const line of csv.trim().split('\n')) { + linenum++; + + try { + const acct = line.split(',')[0].trim(); + const { username, host } = parseAcct(acct); + + let target = isSelfHost(host!) ? await Users.findOne({ + host: null, + usernameLower: username.toLowerCase() + }) : await Users.findOne({ + host: toPuny(host!), + usernameLower: username.toLowerCase() + }); + + if (host == null && target == null) continue; + + if (target == null) { + target = await resolveUser(username, host); + } + + if (target == null) { + throw `cannot resolve user: @${username}@${host}`; + } + + // skip myself + if (target.id === job.data.user.id) continue; + + logger.info(`Mute[${linenum}] ${target.id} ...`); + + await block(user, target); + } catch (e) { + logger.warn(`Error in line:${linenum} ${e}`); + } + } + + logger.succ('Imported'); + done(); +} + diff --git a/src/queue/processors/db/import-muting.ts b/src/queue/processors/db/import-muting.ts new file mode 100644 index 0000000000..798f03a627 --- /dev/null +++ b/src/queue/processors/db/import-muting.ts @@ -0,0 +1,83 @@ +import * as Bull from 'bull'; + +import { queueLogger } from '../../logger'; +import { parseAcct } from '@/misc/acct'; +import { resolveUser } from '@/remote/resolve-user'; +import { downloadTextFile } from '@/misc/download-text-file'; +import { isSelfHost, toPuny } from '@/misc/convert-host'; +import { Users, DriveFiles, Mutings } from '@/models/index'; +import { DbUserImportJobData } from '@/queue/types'; +import { User } from '@/models/entities/user'; +import { genId } from '@/misc/gen-id'; + +const logger = queueLogger.createSubLogger('import-muting'); + +export async function importMuting(job: Bull.Job, done: any): Promise { + logger.info(`Importing muting of ${job.data.user.id} ...`); + + const user = await Users.findOne(job.data.user.id); + if (user == null) { + done(); + return; + } + + const file = await DriveFiles.findOne({ + id: job.data.fileId + }); + if (file == null) { + done(); + return; + } + + const csv = await downloadTextFile(file.url); + + let linenum = 0; + + for (const line of csv.trim().split('\n')) { + linenum++; + + try { + const acct = line.split(',')[0].trim(); + const { username, host } = parseAcct(acct); + + let target = isSelfHost(host!) ? await Users.findOne({ + host: null, + usernameLower: username.toLowerCase() + }) : await Users.findOne({ + host: toPuny(host!), + usernameLower: username.toLowerCase() + }); + + if (host == null && target == null) continue; + + if (target == null) { + target = await resolveUser(username, host); + } + + if (target == null) { + throw `cannot resolve user: @${username}@${host}`; + } + + // skip myself + if (target.id === job.data.user.id) continue; + + logger.info(`Mute[${linenum}] ${target.id} ...`); + + await mute(user, target); + } catch (e) { + logger.warn(`Error in line:${linenum} ${e}`); + } + } + + logger.succ('Imported'); + done(); +} + +async function mute(user: User, target: User) { + await Mutings.insert({ + id: genId(), + createdAt: new Date(), + muterId: user.id, + muteeId: target.id, + }); +} diff --git a/src/queue/processors/db/index.ts b/src/queue/processors/db/index.ts index b051a28e0b..97087642b7 100644 --- a/src/queue/processors/db/index.ts +++ b/src/queue/processors/db/index.ts @@ -9,6 +9,8 @@ import { exportUserLists } from './export-user-lists'; import { importFollowing } from './import-following'; import { importUserLists } from './import-user-lists'; import { deleteAccount } from './delete-account'; +import { importMuting } from './import-muting'; +import { importBlocking } from './import-blocking'; const jobs = { deleteDriveFiles, @@ -18,6 +20,8 @@ const jobs = { exportBlocking, exportUserLists, importFollowing, + importMuting, + importBlocking, importUserLists, deleteAccount, } as Record | Bull.ProcessPromiseFunction>; diff --git a/src/server/api/endpoints/i/import-blocking.ts b/src/server/api/endpoints/i/import-blocking.ts new file mode 100644 index 0000000000..d44d0b6077 --- /dev/null +++ b/src/server/api/endpoints/i/import-blocking.ts @@ -0,0 +1,60 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../define'; +import { createImportBlockingJob } from '@/queue/index'; +import * as ms from 'ms'; +import { ApiError } from '../../error'; +import { DriveFiles } from '@/models/index'; + +export const meta = { + secure: true, + requireCredential: true as const, + + limit: { + duration: ms('1hour'), + max: 1, + }, + + params: { + fileId: { + validator: $.type(ID), + } + }, + + errors: { + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: 'ebb53e5f-6574-9c0c-0b92-7ca6def56d7e' + }, + + unexpectedFileType: { + message: 'We need csv file.', + code: 'UNEXPECTED_FILE_TYPE', + id: 'b6fab7d6-d945-d67c-dfdb-32da1cd12cfe' + }, + + tooBigFile: { + message: 'That file is too big.', + code: 'TOO_BIG_FILE', + id: 'b7fbf0b1-aeef-3b21-29ef-fadd4cb72ccf' + }, + + emptyFile: { + message: 'That file is empty.', + code: 'EMPTY_FILE', + id: '6f3a4dcc-f060-a707-4950-806fbdbe60d6' + }, + } +}; + +export default define(meta, async (ps, user) => { + const file = await DriveFiles.findOne(ps.fileId); + + if (file == null) throw new ApiError(meta.errors.noSuchFile); + //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); + if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); + if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + + createImportBlockingJob(user, file.id); +}); diff --git a/src/server/api/endpoints/i/import-muting.ts b/src/server/api/endpoints/i/import-muting.ts new file mode 100644 index 0000000000..c17434c587 --- /dev/null +++ b/src/server/api/endpoints/i/import-muting.ts @@ -0,0 +1,60 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../define'; +import { createImportMutingJob } from '@/queue/index'; +import * as ms from 'ms'; +import { ApiError } from '../../error'; +import { DriveFiles } from '@/models/index'; + +export const meta = { + secure: true, + requireCredential: true as const, + + limit: { + duration: ms('1hour'), + max: 1, + }, + + params: { + fileId: { + validator: $.type(ID), + } + }, + + errors: { + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: 'e674141e-bd2a-ba85-e616-aefb187c9c2a' + }, + + unexpectedFileType: { + message: 'We need csv file.', + code: 'UNEXPECTED_FILE_TYPE', + id: '568c6e42-c86c-ba09-c004-517f83f9f1a8' + }, + + tooBigFile: { + message: 'That file is too big.', + code: 'TOO_BIG_FILE', + id: '9b4ada6d-d7f7-0472-0713-4f558bd1ec9c' + }, + + emptyFile: { + message: 'That file is empty.', + code: 'EMPTY_FILE', + id: 'd2f12af1-e7b4-feac-86a3-519548f2728e' + }, + } +}; + +export default define(meta, async (ps, user) => { + const file = await DriveFiles.findOne(ps.fileId); + + if (file == null) throw new ApiError(meta.errors.noSuchFile); + //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); + if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); + if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + + createImportMutingJob(user, file.id); +}); From d3c7ddd2f43e5934e099fa76643d3c55ad2ff74b Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 20 Oct 2021 01:12:19 +0900 Subject: [PATCH 07/12] typo --- src/queue/processors/db/import-blocking.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/queue/processors/db/import-blocking.ts b/src/queue/processors/db/import-blocking.ts index ab3b91fc0e..9951da669d 100644 --- a/src/queue/processors/db/import-blocking.ts +++ b/src/queue/processors/db/import-blocking.ts @@ -60,7 +60,7 @@ export async function importBlocking(job: Bull.Job, done: a // skip myself if (target.id === job.data.user.id) continue; - logger.info(`Mute[${linenum}] ${target.id} ...`); + logger.info(`Block[${linenum}] ${target.id} ...`); await block(user, target); } catch (e) { From 202cb38c40cd07137290377fc9274d67c289efba Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 20 Oct 2021 01:15:14 +0900 Subject: [PATCH 08/12] lint --- .../api/endpoints/users/search-by-username-and-host.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/api/endpoints/users/search-by-username-and-host.ts b/src/server/api/endpoints/users/search-by-username-and-host.ts index f03a55449d..1ec5e1a743 100644 --- a/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -91,14 +91,14 @@ export default define(meta, async (ps, me) => { .andWhere('user.isSuspended = FALSE') .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) .andWhere('user.updatedAt IS NOT NULL'); - + otherQuery.setParameters(followingQuery.getParameters()); - + const otherUsers = await otherQuery .orderBy('user.updatedAt', 'DESC') .take(ps.limit! - users.length) .getMany(); - + users = users.concat(otherUsers); } } else { From ea8e6d88aba5b163cf843736fa77dd398418e031 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 20 Oct 2021 03:10:36 +0900 Subject: [PATCH 09/12] =?UTF-8?q?enhance:=20share=E3=83=9A=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=81=A7=E3=82=88=E3=82=8A=E5=A4=9A=E3=81=8F=E3=81=AE?= =?UTF-8?q?=E6=83=85=E5=A0=B1=E3=82=92=E6=B8=A1=E3=81=9B=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=20(#7606)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * shareでより多くの情報を渡せるように * from chat ui post-form, remove instant and add share * fix await eating array, make document * add changelog * https://github.com/misskey-dev/misskey/pull/7606/files/3581bf9a060742dc59bf7fb8ea7316809cc60522#r692265037 * reply, renoteにも型定義 * :art: * 閉じなければ100ms後タイムラインに --- CHANGELOG.md | 1 + src/client/components/post-form.vue | 38 ++++++- src/client/pages/share.vue | 158 ++++++++++++++++++++++---- src/client/ui/chat/post-form.vue | 6 +- src/docs/ja-JP/advanced/share-page.md | 56 +++++++++ src/misc/acct.ts | 9 +- 6 files changed, 233 insertions(+), 35 deletions(-) create mode 100644 src/docs/ja-JP/advanced/share-page.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a3d9a1caa..10b1cdb37d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -131,6 +131,7 @@ - ワードミュートのドキュメントを追加 - クライアントのデザインの調整 - 依存関係の更新 +- /share のクエリでリプライやファイル等の情報を渡せるように ### Bugfixes - チャンネルを作成しているとアカウントを削除できないのを修正 diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue index a1d89d2a2e..816a69e731 100644 --- a/src/client/components/post-form.vue +++ b/src/client/components/post-form.vue @@ -117,11 +117,28 @@ export default defineComponent({ type: String, required: false }, + initialVisibility: { + type: String, + required: false + }, + initialFiles: { + type: Array, + required: false + }, + initialLocalOnly: { + type: Boolean, + required: false + }, + visibleUsers: { + type: Array, + required: false, + default: () => [] + }, initialNote: { type: Object, required: false }, - instant: { + share: { type: Boolean, required: false, default: false @@ -150,8 +167,7 @@ export default defineComponent({ showPreview: false, cw: null, localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly, - visibility: this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility, - visibleUsers: [], + visibility: (this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility) as typeof noteVisibilities[number], autocomplete: null, draghover: false, quoteId: null, @@ -246,6 +262,18 @@ export default defineComponent({ this.text = this.initialText; } + if (this.initialVisibility) { + this.visibility = this.initialVisibility; + } + + if (this.initialFiles) { + this.files = this.initialFiles; + } + + if (typeof this.initialLocalOnly === 'boolean') { + this.localOnly = this.initialLocalOnly; + } + if (this.mention) { this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`; this.text += ' '; @@ -321,7 +349,7 @@ export default defineComponent({ this.$nextTick(() => { // 書きかけの投稿を復元 - if (!this.instant && !this.mention && !this.specified) { + if (!this.share && !this.mention && !this.specified) { const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey]; if (draft) { this.text = draft.data.text; @@ -582,8 +610,6 @@ export default defineComponent({ }, saveDraft() { - if (this.instant) return; - const data = JSON.parse(localStorage.getItem('drafts') || '{}'); data[this.draftKey] = { diff --git a/src/client/pages/share.vue b/src/client/pages/share.vue index 67e598fa8f..70a9661dd0 100644 --- a/src/client/pages/share.vue +++ b/src/client/pages/share.vue @@ -1,22 +1,38 @@ diff --git a/src/client/ui/chat/post-form.vue b/src/client/ui/chat/post-form.vue index 0cacaf77e7..64b8d08cbc 100644 --- a/src/client/ui/chat/post-form.vue +++ b/src/client/ui/chat/post-form.vue @@ -100,7 +100,7 @@ export default defineComponent({ type: Object, required: false }, - instant: { + share: { type: Boolean, required: false, default: false @@ -277,7 +277,7 @@ export default defineComponent({ this.$nextTick(() => { // 書きかけの投稿を復元 - if (!this.instant && !this.mention && !this.specified) { + if (!this.share && !this.mention && !this.specified) { const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey]; if (draft) { this.text = draft.data.text; @@ -507,8 +507,6 @@ export default defineComponent({ }, saveDraft() { - if (this.instant) return; - const data = JSON.parse(localStorage.getItem('drafts') || '{}'); data[this.draftKey] = { diff --git a/src/docs/ja-JP/advanced/share-page.md b/src/docs/ja-JP/advanced/share-page.md new file mode 100644 index 0000000000..75a9d14d29 --- /dev/null +++ b/src/docs/ja-JP/advanced/share-page.md @@ -0,0 +1,56 @@ +# シェアページ +`/share`を開くと、共有用の投稿フォームを開くことができます。 +ここではシェアページで利用できるクエリ文字列の一覧を示します。 + +## クエリ文字列一覧 +### 文字 + +
+
title
+
タイトルです。本文の先頭に[ … ]と挿入されます。
+
text
+
本文です。
+
url
+
URLです。末尾に挿入されます。
+
+ + +### リプライ情報 +以下のいずれか + +
+
replyId
+
リプライ先のノートid
+
replyUri
+
リプライ先のUrl(リモートのノートオブジェクトを指定)
+
+ +### Renote情報 +以下のいずれか + +
+
renoteId
+
Renote先のノートid
+
renoteUri
+
Renote先のUrl(リモートのノートオブジェクトを指定)
+
+ +### 公開範囲 +※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する + +
+
visibility
+
公開範囲 ['public' | 'home' | 'followers' | 'specified']
+
localOnly
+
0(false) or 1(true)
+
visibleUserIds
+
specified時のダイレクト先のユーザーid カンマ区切りで
+
visibleAccts
+
specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで
+
+ +### ファイル +
+
fileIds
+
添付したいファイルのid(カンマ区切りで)
+
diff --git a/src/misc/acct.ts b/src/misc/acct.ts index 16876c4429..5106b1a09e 100644 --- a/src/misc/acct.ts +++ b/src/misc/acct.ts @@ -1,13 +1,10 @@ -export type Acct = { - username: string; - host: string | null; -}; +import * as Misskey from 'misskey-js'; -export const getAcct = (user: Acct) => { +export const getAcct = (user: Misskey.Acct) => { return user.host == null ? user.username : `${user.username}@${user.host}`; }; -export const parseAcct = (acct: string): Acct => { +export const parseAcct = (acct: string): Misskey.Acct => { if (acct.startsWith('@')) acct = acct.substr(1); const split = acct.split('@', 2); return { username: split[0], host: split[1] || null }; From a4e31366119d2d115ab951f74e508ccf73197306 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 20 Oct 2021 03:12:20 +0900 Subject: [PATCH 10/12] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10b1cdb37d..7a58015f4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - API: ユーザーのリアクション一覧を取得する users/reactions を追加 - API: users/search および users/search-by-username-and-host を強化 - ミュート及びブロックのインポートを行えるように +- クライアント: /share のクエリでリプライやファイル等の情報を渡せるように ### Bugfixes - クライアント: テーマの管理が行えない問題を修正 @@ -131,7 +132,6 @@ - ワードミュートのドキュメントを追加 - クライアントのデザインの調整 - 依存関係の更新 -- /share のクエリでリプライやファイル等の情報を渡せるように ### Bugfixes - チャンネルを作成しているとアカウントを削除できないのを修正 From 98c26dfff80903dc082179a7df91d7a19af9fb08 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 21 Oct 2021 00:28:27 +0900 Subject: [PATCH 11/12] =?UTF-8?q?fix(activitypub):=20not=20reacted=20?= =?UTF-8?q?=E3=81=AA=20Undo.Like=20=E3=81=8Cinbox=E3=81=AB=E6=BB=9E?= =?UTF-8?q?=E7=95=99=E3=81=99=E3=82=8B=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https: //github.com/mei23/misskey/commit/1cfb5e09a44819b82333df26409ec9d9657bdcfc Co-Authored-By: MeiMei <30769358+mei23@users.noreply.github.com> --- CHANGELOG.md | 1 + src/remote/activitypub/kernel/undo/like.ts | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a3d9a1caa..ec38ad9bdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ ### Bugfixes - クライアント: テーマの管理が行えない問題を修正 - API: アプリケーション通知が取得できない問題を修正 +- ActivityPub: not reacted な Undo.Like がinboxに滞留するのを修正 ## 12.92.0 (2021/10/16) diff --git a/src/remote/activitypub/kernel/undo/like.ts b/src/remote/activitypub/kernel/undo/like.ts index 7f821cada0..107d3053e3 100644 --- a/src/remote/activitypub/kernel/undo/like.ts +++ b/src/remote/activitypub/kernel/undo/like.ts @@ -12,6 +12,10 @@ export default async (actor: IRemoteUser, activity: ILike) => { const note = await fetchNote(targetUri); if (!note) return `skip: target note not found ${targetUri}`; - await deleteReaction(actor, note); + await deleteReaction(actor, note).catch(e => { + if (e.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') return; + throw e; + }); + return `ok`; }; From 69b56f6658dcbef0a54fa030ebf30913ca3d30bd Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 21 Oct 2021 01:04:10 +0900 Subject: [PATCH 12/12] =?UTF-8?q?refactor:=20publishHogeStream=E3=81=A8Str?= =?UTF-8?q?eam=E3=81=AEEventEmitter=E3=81=AB=E5=9E=8B=E5=AE=9A=E7=BE=A9?= =?UTF-8?q?=E3=81=99=E3=82=8B=20(#7769)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * wip * wip * :v: * add main stream * packedNotificationSchemaを更新 * read:gallery, write:gallery, read:gallery-likes, write:gallery-likesに翻訳を追加 * fix * ok * add header, choice, invitation * add header, choice, invitation * test * fix * fix * yatta * remove no longer needed "as PackedUser/PackedNote" * clean up * add simple-schema * fix lint * fix lint * wip * wip! * wip * fix * wip * wip * :v: * 送信側に型エラーがないことを3回確認した * :v: * wip * update typescript * define items in full Schema * edit comment * edit comment * edit comment * Update src/prelude/types.ts Co-authored-by: Acid Chicken (硫酸鶏) * https://github.com/misskey-dev/misskey/pull/7769#discussion_r703058458 * user packとnote packの型不整合を修正 * revert https://github.com/misskey-dev/misskey/pull/7772#discussion_r706627736 * revert https://github.com/misskey-dev/misskey/pull/7772#discussion_r706627736 * user packとnote packの型不整合を修正 * add prelude/types.ts * emoji * signin * game * matching * clean up * ev => data * refactor * clean up * add type * antenna * channel * fix * add Packed type * add PackedRef * fix lint * add emoji schema * add reversiGame * add reversiMatching * remove signin schema (use Signin entity) * add schemas refs, fix Packed type * wip PackedHoge => Packed<'Hoge'> * add Packed type * note-reaction * user * user-group * user-list * note * app, messaging-message * notification * drive-file * drive-folder * following * muting * blocking * hashtag * page * app (with modifying schema) * import user? * channel * antenna * clip * gallery-post * emoji * Packed * reversi-matching * update stream.ts * https://github.com/misskey-dev/misskey/pull/7769#issuecomment-917542339 * fix lint * clean up? * add changelog * add changelog * add changelog * fix: アンテナが既読にならないのを修正 * revert fix * https://github.com/misskey-dev/misskey/pull/7769#discussion_r711474875 * spec => payload * edit commetn Co-authored-by: Acid Chicken (硫酸鶏) --- package.json | 1 + src/models/repositories/signin.ts | 2 +- .../api/common/read-messaging-message.ts | 2 +- src/server/api/endpoints/antennas/update.ts | 2 +- src/server/api/stream/channels/antenna.ts | 11 +- src/server/api/stream/channels/channel.ts | 3 +- src/server/api/stream/channels/main.ts | 24 +- src/server/api/stream/channels/messaging.ts | 6 +- src/server/api/stream/index.ts | 35 +- src/server/api/stream/types.ts | 299 ++++++++++++++++++ src/services/stream.ts | 53 +++- yarn.lock | 5 + 12 files changed, 383 insertions(+), 60 deletions(-) create mode 100644 src/server/api/stream/types.ts diff --git a/package.json b/package.json index c1fcff7126..10ddea2c1e 100644 --- a/package.json +++ b/package.json @@ -208,6 +208,7 @@ "seedrandom": "3.0.5", "sharp": "0.29.1", "speakeasy": "2.0.0", + "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "style-loader": "3.3.0", "summaly": "2.4.1", diff --git a/src/models/repositories/signin.ts b/src/models/repositories/signin.ts index 9942d2d962..f375f9b5c0 100644 --- a/src/models/repositories/signin.ts +++ b/src/models/repositories/signin.ts @@ -4,7 +4,7 @@ import { Signin } from '@/models/entities/signin'; @EntityRepository(Signin) export class SigninRepository extends Repository { public async pack( - src: any, + src: Signin, ) { return src; } diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts index 1dce76d2a9..33f41b2770 100644 --- a/src/server/api/common/read-messaging-message.ts +++ b/src/server/api/common/read-messaging-message.ts @@ -77,7 +77,7 @@ export async function readGroupMessagingMessage( id: In(messageIds) }); - const reads = []; + const reads: MessagingMessage['id'][] = []; for (const message of messages) { if (message.userId === userId) continue; diff --git a/src/server/api/endpoints/antennas/update.ts b/src/server/api/endpoints/antennas/update.ts index ff13e89bcc..d69b4feee6 100644 --- a/src/server/api/endpoints/antennas/update.ts +++ b/src/server/api/endpoints/antennas/update.ts @@ -137,7 +137,7 @@ export default define(meta, async (ps, user) => { notify: ps.notify, }); - publishInternalEvent('antennaUpdated', Antennas.findOneOrFail(antenna.id)); + publishInternalEvent('antennaUpdated', await Antennas.findOneOrFail(antenna.id)); return await Antennas.pack(antenna.id); }); diff --git a/src/server/api/stream/channels/antenna.ts b/src/server/api/stream/channels/antenna.ts index bf9c53c453..3cbdfebb43 100644 --- a/src/server/api/stream/channels/antenna.ts +++ b/src/server/api/stream/channels/antenna.ts @@ -3,6 +3,7 @@ import Channel from '../channel'; import { Notes } from '@/models/index'; import { isMutedUserRelated } from '@/misc/is-muted-user-related'; import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; +import { StreamMessages } from '../types'; export default class extends Channel { public readonly chName = 'antenna'; @@ -19,11 +20,9 @@ export default class extends Channel { } @autobind - private async onEvent(data: any) { - const { type, body } = data; - - if (type === 'note') { - const note = await Notes.pack(body.id, this.user, { detail: true }); + private async onEvent(data: StreamMessages['antenna']['payload']) { + if (data.type === 'note') { + const note = await Notes.pack(data.body.id, this.user, { detail: true }); // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (isMutedUserRelated(note, this.muting)) return; @@ -34,7 +33,7 @@ export default class extends Channel { this.send('note', note); } else { - this.send(type, body); + this.send(data.type, data.body); } } diff --git a/src/server/api/stream/channels/channel.ts b/src/server/api/stream/channels/channel.ts index 72ddbf93b4..bf7942f522 100644 --- a/src/server/api/stream/channels/channel.ts +++ b/src/server/api/stream/channels/channel.ts @@ -4,6 +4,7 @@ import { Notes, Users } from '@/models/index'; import { isMutedUserRelated } from '@/misc/is-muted-user-related'; import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; import { User } from '@/models/entities/user'; +import { StreamMessages } from '../types'; import { Packed } from '@/misc/schema'; export default class extends Channel { @@ -52,7 +53,7 @@ export default class extends Channel { } @autobind - private onEvent(data: any) { + private onEvent(data: StreamMessages['channel']['payload']) { if (data.type === 'typing') { const id = data.body; const begin = this.typers[id] == null; diff --git a/src/server/api/stream/channels/main.ts b/src/server/api/stream/channels/main.ts index b99cb931da..131ac30472 100644 --- a/src/server/api/stream/channels/main.ts +++ b/src/server/api/stream/channels/main.ts @@ -11,35 +11,33 @@ export default class extends Channel { public async init(params: any) { // Subscribe main stream channel this.subscriber.on(`mainStream:${this.user!.id}`, async data => { - const { type } = data; - let { body } = data; - - switch (type) { + switch (data.type) { case 'notification': { - if (this.muting.has(body.userId)) return; - if (body.note && body.note.isHidden) { - const note = await Notes.pack(body.note.id, this.user, { + if (data.body.userId && this.muting.has(data.body.userId)) return; + + if (data.body.note && data.body.note.isHidden) { + const note = await Notes.pack(data.body.note.id, this.user, { detail: true }); this.connection.cacheNote(note); - body.note = note; + data.body.note = note; } break; } case 'mention': { - if (this.muting.has(body.userId)) return; - if (body.isHidden) { - const note = await Notes.pack(body.id, this.user, { + if (this.muting.has(data.body.userId)) return; + if (data.body.isHidden) { + const note = await Notes.pack(data.body.id, this.user, { detail: true }); this.connection.cacheNote(note); - body = note; + data.body = note; } break; } } - this.send(type, body); + this.send(data.type, data.body); }); } } diff --git a/src/server/api/stream/channels/messaging.ts b/src/server/api/stream/channels/messaging.ts index 015b0a7650..c049e880b9 100644 --- a/src/server/api/stream/channels/messaging.ts +++ b/src/server/api/stream/channels/messaging.ts @@ -3,6 +3,8 @@ import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivit import Channel from '../channel'; import { UserGroupJoinings, Users, MessagingMessages } from '@/models/index'; import { User, ILocalUser, IRemoteUser } from '@/models/entities/user'; +import { UserGroup } from '@/models/entities/user-group'; +import { StreamMessages } from '../types'; export default class extends Channel { public readonly chName = 'messaging'; @@ -12,7 +14,7 @@ export default class extends Channel { private otherpartyId: string | null; private otherparty: User | null; private groupId: string | null; - private subCh: string; + private subCh: `messagingStream:${User['id']}-${User['id']}` | `messagingStream:${UserGroup['id']}`; private typers: Record = {}; private emitTypersIntervalId: ReturnType; @@ -45,7 +47,7 @@ export default class extends Channel { } @autobind - private onEvent(data: any) { + private onEvent(data: StreamMessages['messaging']['payload'] | StreamMessages['groupMessaging']['payload']) { if (data.type === 'typing') { const id = data.body; const begin = this.typers[id] == null; diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts index ccd555e149..da4ea5ec99 100644 --- a/src/server/api/stream/index.ts +++ b/src/server/api/stream/index.ts @@ -14,6 +14,7 @@ import { AccessToken } from '@/models/entities/access-token'; import { UserProfile } from '@/models/entities/user-profile'; import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '@/services/stream'; import { UserGroup } from '@/models/entities/user-group'; +import { StreamEventEmitter, StreamMessages } from './types'; import { Packed } from '@/misc/schema'; /** @@ -28,7 +29,7 @@ export default class Connection { public followingChannels: Set = new Set(); public token?: AccessToken; private wsConnection: websocket.connection; - public subscriber: EventEmitter; + public subscriber: StreamEventEmitter; private channels: Channel[] = []; private subscribingNotes: any = {}; private cachedNotes: Packed<'Note'>[] = []; @@ -46,8 +47,8 @@ export default class Connection { this.wsConnection.on('message', this.onWsConnectionMessage); - this.subscriber.on('broadcast', async ({ type, body }) => { - this.onBroadcastMessage(type, body); + this.subscriber.on('broadcast', data => { + this.onBroadcastMessage(data); }); if (this.user) { @@ -57,43 +58,41 @@ export default class Connection { this.updateFollowingChannels(); this.updateUserProfile(); - this.subscriber.on(`user:${this.user.id}`, ({ type, body }) => { - this.onUserEvent(type, body); - }); + this.subscriber.on(`user:${this.user.id}`, this.onUserEvent); } } @autobind - private onUserEvent(type: string, body: any) { - switch (type) { + private onUserEvent(data: StreamMessages['user']['payload']) { // { type, body }と展開するとそれぞれ型が分離してしまう + switch (data.type) { case 'follow': - this.following.add(body.id); + this.following.add(data.body.id); break; case 'unfollow': - this.following.delete(body.id); + this.following.delete(data.body.id); break; case 'mute': - this.muting.add(body.id); + this.muting.add(data.body.id); break; case 'unmute': - this.muting.delete(body.id); + this.muting.delete(data.body.id); break; // TODO: block events case 'followChannel': - this.followingChannels.add(body.id); + this.followingChannels.add(data.body.id); break; case 'unfollowChannel': - this.followingChannels.delete(body.id); + this.followingChannels.delete(data.body.id); break; case 'updateUserProfile': - this.userProfile = body; + this.userProfile = data.body; break; case 'terminate': @@ -145,8 +144,8 @@ export default class Connection { } @autobind - private onBroadcastMessage(type: string, body: any) { - this.sendMessageToWs(type, body); + private onBroadcastMessage(data: StreamMessages['broadcast']['payload']) { + this.sendMessageToWs(data.type, data.body); } @autobind @@ -249,7 +248,7 @@ export default class Connection { } @autobind - private async onNoteStreamMessage(data: any) { + private async onNoteStreamMessage(data: StreamMessages['note']['payload']) { this.sendMessageToWs('noteUpdated', { id: data.body.id, type: data.type, diff --git a/src/server/api/stream/types.ts b/src/server/api/stream/types.ts new file mode 100644 index 0000000000..70eb5c5ce5 --- /dev/null +++ b/src/server/api/stream/types.ts @@ -0,0 +1,299 @@ +import { EventEmitter } from 'events'; +import Emitter from 'strict-event-emitter-types'; +import { Channel } from '@/models/entities/channel'; +import { User } from '@/models/entities/user'; +import { UserProfile } from '@/models/entities/user-profile'; +import { Note } from '@/models/entities/note'; +import { Antenna } from '@/models/entities/antenna'; +import { DriveFile } from '@/models/entities/drive-file'; +import { DriveFolder } from '@/models/entities/drive-folder'; +import { Emoji } from '@/models/entities/emoji'; +import { UserList } from '@/models/entities/user-list'; +import { MessagingMessage } from '@/models/entities/messaging-message'; +import { UserGroup } from '@/models/entities/user-group'; +import { ReversiGame } from '@/models/entities/games/reversi/game'; +import { AbuseUserReport } from '@/models/entities/abuse-user-report'; +import { Signin } from '@/models/entities/signin'; +import { Page } from '@/models/entities/page'; +import { Packed } from '@/misc/schema'; + +//#region Stream type-body definitions +export interface InternalStreamTypes { + antennaCreated: Antenna; + antennaDeleted: Antenna; + antennaUpdated: Antenna; +} + +export interface BroadcastTypes { + emojiAdded: { + emoji: Packed<'Emoji'>; + }; +} + +export interface UserStreamTypes { + terminate: {}; + followChannel: Channel; + unfollowChannel: Channel; + updateUserProfile: UserProfile; + mute: User; + unmute: User; + follow: Packed<'User'>; + unfollow: Packed<'User'>; + userAdded: Packed<'User'>; +} + +export interface MainStreamTypes { + notification: Packed<'Notification'>; + mention: Packed<'Note'>; + reply: Packed<'Note'>; + renote: Packed<'Note'>; + follow: Packed<'User'>; + followed: Packed<'User'>; + unfollow: Packed<'User'>; + meUpdated: Packed<'User'>; + pageEvent: { + pageId: Page['id']; + event: string; + var: any; + userId: User['id']; + user: Packed<'User'>; + }; + urlUploadFinished: { + marker?: string | null; + file: Packed<'DriveFile'>; + }; + readAllNotifications: undefined; + unreadNotification: Packed<'Notification'>; + unreadMention: Note['id']; + readAllUnreadMentions: undefined; + unreadSpecifiedNote: Note['id']; + readAllUnreadSpecifiedNotes: undefined; + readAllMessagingMessages: undefined; + messagingMessage: Packed<'MessagingMessage'>; + unreadMessagingMessage: Packed<'MessagingMessage'>; + readAllAntennas: undefined; + unreadAntenna: Antenna; + readAllAnnouncements: undefined; + readAllChannels: undefined; + unreadChannel: Note['id']; + myTokenRegenerated: undefined; + reversiNoInvites: undefined; + reversiInvited: Packed<'ReversiMatching'>; + signin: Signin; + registryUpdated: { + scope?: string[]; + key: string; + value: any | null; + }; + driveFileCreated: Packed<'DriveFile'>; + readAntenna: Antenna; +} + +export interface DriveStreamTypes { + fileCreated: Packed<'DriveFile'>; + fileDeleted: DriveFile['id']; + fileUpdated: Packed<'DriveFile'>; + folderCreated: Packed<'DriveFolder'>; + folderDeleted: DriveFolder['id']; + folderUpdated: Packed<'DriveFolder'>; +} + +export interface NoteStreamTypes { + pollVoted: { + choice: number; + userId: User['id']; + }; + deleted: { + deletedAt: Date; + }; + reacted: { + reaction: string; + emoji?: Emoji; + userId: User['id']; + }; + unreacted: { + reaction: string; + userId: User['id']; + }; +} +type NoteStreamEventTypes = { + [key in keyof NoteStreamTypes]: { + id: Note['id']; + body: NoteStreamTypes[key]; + }; +}; + +export interface ChannelStreamTypes { + typing: User['id']; +} + +export interface UserListStreamTypes { + userAdded: Packed<'User'>; + userRemoved: Packed<'User'>; +} + +export interface AntennaStreamTypes { + note: Note; +} + +export interface MessagingStreamTypes { + read: MessagingMessage['id'][]; + typing: User['id']; + message: Packed<'MessagingMessage'>; + deleted: MessagingMessage['id']; +} + +export interface GroupMessagingStreamTypes { + read: { + ids: MessagingMessage['id'][]; + userId: User['id']; + }; + typing: User['id']; + message: Packed<'MessagingMessage'>; + deleted: MessagingMessage['id']; +} + +export interface MessagingIndexStreamTypes { + read: MessagingMessage['id'][]; + message: Packed<'MessagingMessage'>; +} + +export interface ReversiStreamTypes { + matched: Packed<'ReversiGame'>; + invited: Packed<'ReversiMatching'>; +} + +export interface ReversiGameStreamTypes { + started: Packed<'ReversiGame'>; + ended: { + winnerId?: User['id'] | null, + game: Packed<'ReversiGame'>; + }; + updateSettings: { + key: string; + value: FIXME; + }; + initForm: { + userId: User['id']; + form: FIXME; + }; + updateForm: { + userId: User['id']; + id: string; + value: FIXME; + }; + message: { + userId: User['id']; + message: FIXME; + }; + changeAccepts: { + user1: boolean; + user2: boolean; + }; + set: { + at: Date; + color: boolean; + pos: number; + next: boolean; + }; + watching: User['id']; +} + +export interface AdminStreamTypes { + newAbuseUserReport: { + id: AbuseUserReport['id']; + targetUserId: User['id'], + reporterId: User['id'], + comment: string; + }; +} +//#endregion + +// 辞書(interface or type)から{ type, body }ユニオンを定義 +// https://stackoverflow.com/questions/49311989/can-i-infer-the-type-of-a-value-using-extends-keyof-type +// VS Codeの展開を防止するためにEvents型を定義 +type Events = { [K in keyof T]: { type: K; body: T[K]; } }; +type EventUnionFromDictionary< + T extends object, + U = Events +> = U[keyof U]; + +// name/messages(spec) pairs dictionary +export type StreamMessages = { + internal: { + name: 'internal'; + payload: EventUnionFromDictionary; + }; + broadcast: { + name: 'broadcast'; + payload: EventUnionFromDictionary; + }; + user: { + name: `user:${User['id']}`; + payload: EventUnionFromDictionary; + }; + main: { + name: `mainStream:${User['id']}`; + payload: EventUnionFromDictionary; + }; + drive: { + name: `driveStream:${User['id']}`; + payload: EventUnionFromDictionary; + }; + note: { + name: `noteStream:${Note['id']}`; + payload: EventUnionFromDictionary; + }; + channel: { + name: `channelStream:${Channel['id']}`; + payload: EventUnionFromDictionary; + }; + userList: { + name: `userListStream:${UserList['id']}`; + payload: EventUnionFromDictionary; + }; + antenna: { + name: `antennaStream:${Antenna['id']}`; + payload: EventUnionFromDictionary; + }; + messaging: { + name: `messagingStream:${User['id']}-${User['id']}`; + payload: EventUnionFromDictionary; + }; + groupMessaging: { + name: `messagingStream:${UserGroup['id']}`; + payload: EventUnionFromDictionary; + }; + messagingIndex: { + name: `messagingIndexStream:${User['id']}`; + payload: EventUnionFromDictionary; + }; + reversi: { + name: `reversiStream:${User['id']}`; + payload: EventUnionFromDictionary; + }; + reversiGame: { + name: `reversiGameStream:${ReversiGame['id']}`; + payload: EventUnionFromDictionary; + }; + admin: { + name: `adminStream:${User['id']}`; + payload: EventUnionFromDictionary; + }; + notes: { + name: 'notesStream'; + payload: Packed<'Note'>; + }; +}; + +// API event definitions +// ストリームごとのEmitterの辞書を用意 +type EventEmitterDictionary = { [x in keyof StreamMessages]: Emitter void }> }; +// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; +// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする +export type StreamEventEmitter = UnionToIntersection; +// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる + +// provide stream channels union +export type StreamChannels = StreamMessages[keyof StreamMessages]['name']; diff --git a/src/services/stream.ts b/src/services/stream.ts index 4db1a77395..2c308a1b54 100644 --- a/src/services/stream.ts +++ b/src/services/stream.ts @@ -7,9 +7,28 @@ import { UserGroup } from '@/models/entities/user-group'; import config from '@/config/index'; import { Antenna } from '@/models/entities/antenna'; import { Channel } from '@/models/entities/channel'; +import { + StreamChannels, + AdminStreamTypes, + AntennaStreamTypes, + BroadcastTypes, + ChannelStreamTypes, + DriveStreamTypes, + GroupMessagingStreamTypes, + InternalStreamTypes, + MainStreamTypes, + MessagingIndexStreamTypes, + MessagingStreamTypes, + NoteStreamTypes, + ReversiGameStreamTypes, + ReversiStreamTypes, + UserListStreamTypes, + UserStreamTypes +} from '@/server/api/stream/types'; +import { Packed } from '@/misc/schema'; class Publisher { - private publish = (channel: string, type: string | null, value?: any): void => { + private publish = (channel: StreamChannels, type: string | null, value?: any): void => { const message = type == null ? value : value == null ? { type: type, body: null } : { type: type, body: value }; @@ -20,70 +39,70 @@ class Publisher { })); } - public publishInternalEvent = (type: string, value?: any): void => { + public publishInternalEvent = (type: K, value?: InternalStreamTypes[K]): void => { this.publish('internal', type, typeof value === 'undefined' ? null : value); } - public publishUserEvent = (userId: User['id'], type: string, value?: any): void => { + public publishUserEvent = (userId: User['id'], type: K, value?: UserStreamTypes[K]): void => { this.publish(`user:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishBroadcastStream = (type: string, value?: any): void => { + public publishBroadcastStream = (type: K, value?: BroadcastTypes[K]): void => { this.publish('broadcast', type, typeof value === 'undefined' ? null : value); } - public publishMainStream = (userId: User['id'], type: string, value?: any): void => { + public publishMainStream = (userId: User['id'], type: K, value?: MainStreamTypes[K]): void => { this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishDriveStream = (userId: User['id'], type: string, value?: any): void => { + public publishDriveStream = (userId: User['id'], type: K, value?: DriveStreamTypes[K]): void => { this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishNoteStream = (noteId: Note['id'], type: string, value: any): void => { + public publishNoteStream = (noteId: Note['id'], type: K, value?: NoteStreamTypes[K]): void => { this.publish(`noteStream:${noteId}`, type, { id: noteId, body: value }); } - public publishChannelStream = (channelId: Channel['id'], type: string, value?: any): void => { + public publishChannelStream = (channelId: Channel['id'], type: K, value?: ChannelStreamTypes[K]): void => { this.publish(`channelStream:${channelId}`, type, typeof value === 'undefined' ? null : value); } - public publishUserListStream = (listId: UserList['id'], type: string, value?: any): void => { + public publishUserListStream = (listId: UserList['id'], type: K, value?: UserListStreamTypes[K]): void => { this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value); } - public publishAntennaStream = (antennaId: Antenna['id'], type: string, value?: any): void => { + public publishAntennaStream = (antennaId: Antenna['id'], type: K, value?: AntennaStreamTypes[K]): void => { this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value); } - public publishMessagingStream = (userId: User['id'], otherpartyId: User['id'], type: string, value?: any): void => { + public publishMessagingStream = (userId: User['id'], otherpartyId: User['id'], type: K, value?: MessagingStreamTypes[K]): void => { this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); } - public publishGroupMessagingStream = (groupId: UserGroup['id'], type: string, value?: any): void => { + public publishGroupMessagingStream = (groupId: UserGroup['id'], type: K, value?: GroupMessagingStreamTypes[K]): void => { this.publish(`messagingStream:${groupId}`, type, typeof value === 'undefined' ? null : value); } - public publishMessagingIndexStream = (userId: User['id'], type: string, value?: any): void => { + public publishMessagingIndexStream = (userId: User['id'], type: K, value?: MessagingIndexStreamTypes[K]): void => { this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishReversiStream = (userId: User['id'], type: string, value?: any): void => { + public publishReversiStream = (userId: User['id'], type: K, value?: ReversiStreamTypes[K]): void => { this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishReversiGameStream = (gameId: ReversiGame['id'], type: string, value?: any): void => { + public publishReversiGameStream = (gameId: ReversiGame['id'], type: K, value?: ReversiGameStreamTypes[K]): void => { this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value); } - public publishNotesStream = (note: any): void => { + public publishNotesStream = (note: Packed<'Note'>): void => { this.publish('notesStream', null, note); } - public publishAdminStream = (userId: User['id'], type: string, value?: any): void => { + public publishAdminStream = (userId: User['id'], type: K, value?: AdminStreamTypes[K]): void => { this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value); } } diff --git a/yarn.lock b/yarn.lock index cd76b59af6..e2140e185a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10288,6 +10288,11 @@ streamsearch@0.1.2: resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= +strict-event-emitter-types@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz#05e15549cb4da1694478a53543e4e2f4abcf277f" + integrity sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA== + strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"