summaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-10-23 01:08:45 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-10-23 01:08:45 +0900
commitd0d5068f728e13f3ebe1dc227ddaacf380817ec4 (patch)
tree7bb95207e01bff1bee9877829c0556d3ecf62176 /src/server
parentMerge branch 'develop' (diff)
parent12.93.0 (diff)
downloadmisskey-d0d5068f728e13f3ebe1dc227ddaacf380817ec4.tar.gz
misskey-d0d5068f728e13f3ebe1dc227ddaacf380817ec4.tar.bz2
misskey-d0d5068f728e13f3ebe1dc227ddaacf380817ec4.zip
Merge branch 'develop'
Diffstat (limited to 'src/server')
-rw-r--r--src/server/api/common/read-messaging-message.ts2
-rw-r--r--src/server/api/endpoints/admin/logs.ts126
-rw-r--r--src/server/api/endpoints/admin/resync-chart.ts4
-rw-r--r--src/server/api/endpoints/antennas/update.ts2
-rw-r--r--src/server/api/endpoints/blocking/create.ts13
-rw-r--r--src/server/api/endpoints/i/import-blocking.ts60
-rw-r--r--src/server/api/endpoints/i/import-muting.ts60
-rw-r--r--src/server/api/endpoints/i/notifications.ts11
-rw-r--r--src/server/api/endpoints/i/update.ts5
-rw-r--r--src/server/api/endpoints/mute/create.ts2
-rw-r--r--src/server/api/endpoints/users/reactions.ts79
-rw-r--r--src/server/api/endpoints/users/search-by-username-and-host.ts74
-rw-r--r--src/server/api/endpoints/users/search.ts101
-rw-r--r--src/server/api/stream/channels/antenna.ts11
-rw-r--r--src/server/api/stream/channels/channel.ts3
-rw-r--r--src/server/api/stream/channels/main.ts24
-rw-r--r--src/server/api/stream/channels/messaging.ts6
-rw-r--r--src/server/api/stream/index.ts35
-rw-r--r--src/server/api/stream/types.ts299
-rw-r--r--src/server/web/manifest.json2
20 files changed, 668 insertions, 251 deletions
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/admin/logs.ts b/src/server/api/endpoints/admin/logs.ts
deleted file mode 100644
index 776403a62e..0000000000
--- a/src/server/api/endpoints/admin/logs.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { Logs } from '@/models/index';
-import { Brackets } from 'typeorm';
-
-export const meta = {
- tags: ['admin'],
-
- requireCredential: true as const,
- requireModerator: true,
-
- params: {
- limit: {
- validator: $.optional.num.range(1, 100),
- default: 30
- },
-
- level: {
- validator: $.optional.nullable.str,
- default: null
- },
-
- domain: {
- validator: $.optional.nullable.str,
- default: null
- }
- },
-
- res: {
- type: 'array' as const,
- optional: false as const, nullable: false as const,
- items: {
- type: 'object' as const,
- optional: false as const, nullable: false as const,
- properties: {
- id: {
- type: 'string' as const,
- optional: false as const, nullable: false as const,
- format: 'id',
- example: 'xxxxxxxxxx',
- },
- createdAt: {
- type: 'string' as const,
- optional: false as const, nullable: false as const,
- format: 'date-time',
- },
- domain: {
- type: 'array' as const,
- optional: false as const, nullable: false as const,
- items: {
- type: 'string' as const,
- optional: true as const, nullable: false as const
- }
- },
- level: {
- type: 'string' as const,
- optional: false as const, nullable: false as const
- },
- worker: {
- type: 'string' as const,
- optional: false as const, nullable: false as const
- },
- machine: {
- type: 'string' as const,
- optional: false as const, nullable: false as const,
- },
- message: {
- type: 'string' as const,
- optional: false as const, nullable: false as const,
- },
- data: {
- type: 'object' as const,
- optional: false as const, nullable: false as const
- }
- }
- }
- }
-};
-
-export default define(meta, async (ps) => {
- const query = Logs.createQueryBuilder('log');
-
- if (ps.level) query.andWhere('log.level = :level', { level: ps.level });
-
- if (ps.domain) {
- const whiteDomains = ps.domain.split(' ').filter(x => !x.startsWith('-'));
- const blackDomains = ps.domain.split(' ').filter(x => x.startsWith('-')).map(x => x.substr(1));
-
- if (whiteDomains.length > 0) {
- query.andWhere(new Brackets(qb => {
- for (const whiteDomain of whiteDomains) {
- let i = 0;
- for (const subDomain of whiteDomain.split('.')) {
- const p = `whiteSubDomain_${subDomain}_${i}`;
- // SQL is 1 based, so we need '+ 1'
- qb.orWhere(`log.domain[${i + 1}] = :${p}`, { [p]: subDomain });
- i++;
- }
- }
- }));
- }
-
- if (blackDomains.length > 0) {
- query.andWhere(new Brackets(qb => {
- for (const blackDomain of blackDomains) {
- qb.andWhere(new Brackets(qb => {
- const subDomains = blackDomain.split('.');
- let i = 0;
- for (const subDomain of subDomains) {
- const p = `blackSubDomain_${subDomain}_${i}`;
- // 全体で否定できないのでド・モルガンの法則で
- // !(P && Q) を !P || !Q で表す
- // SQL is 1 based, so we need '+ 1'
- qb.orWhere(`log.domain[${i + 1}] != :${p}`, { [p]: subDomain });
- i++;
- }
- }));
- }
- }));
- }
- }
-
- const logs = await query.orderBy('log.createdAt', 'DESC').take(ps.limit!).getMany();
-
- return logs;
-});
diff --git a/src/server/api/endpoints/admin/resync-chart.ts b/src/server/api/endpoints/admin/resync-chart.ts
index b0e687333f..e01dfce1b6 100644
--- a/src/server/api/endpoints/admin/resync-chart.ts
+++ b/src/server/api/endpoints/admin/resync-chart.ts
@@ -1,5 +1,5 @@
import define from '../../define';
-import { driveChart, notesChart, usersChart, instanceChart } from '@/services/chart/index';
+import { driveChart, notesChart, usersChart } from '@/services/chart/index';
import { insertModerationLog } from '@/services/insert-moderation-log';
export const meta = {
@@ -15,7 +15,7 @@ export default define(meta, async (ps, me) => {
driveChart.resync();
notesChart.resync();
usersChart.resync();
- instanceChart.resync();
// TODO: ユーザーごとのチャートもキューに入れて更新する
+ // TODO: インスタンスごとのチャートもキューに入れて更新する
});
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/endpoints/blocking/create.ts b/src/server/api/endpoints/blocking/create.ts
index 4deaa39974..2953252394 100644
--- a/src/server/api/endpoints/blocking/create.ts
+++ b/src/server/api/endpoints/blocking/create.ts
@@ -43,12 +43,6 @@ export const meta = {
code: 'ALREADY_BLOCKING',
id: '787fed64-acb9-464a-82eb-afbd745b9614'
},
-
- cannotBlockModerator: {
- message: 'Cannot block a moderator or an admin.',
- code: 'CANNOT_BLOCK_MODERATOR',
- id: '8544aaef-89fb-e470-9f6c-385d38b474f5'
- }
},
res: {
@@ -82,12 +76,7 @@ export default define(meta, async (ps, user) => {
throw new ApiError(meta.errors.alreadyBlocking);
}
- try {
- await create(blocker, blockee);
- } catch (e) {
- if (e.id === 'e42b7890-5e4d-9d9c-d54b-cf4dd30adfb5') throw new ApiError(meta.errors.cannotBlockModerator);
- throw e;
- }
+ await create(blocker, blockee);
NoteWatchings.delete({
userId: blocker.id,
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);
+});
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 });
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index 9dd637251d..3b8b1579ea 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -68,6 +68,10 @@ export const meta = {
validator: $.optional.bool,
},
+ publicReactions: {
+ validator: $.optional.bool,
+ },
+
carefulBot: {
validator: $.optional.bool,
},
@@ -180,6 +184,7 @@ export default define(meta, async (ps, _user, token) => {
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable;
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.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
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,
diff --git a/src/server/api/endpoints/users/reactions.ts b/src/server/api/endpoints/users/reactions.ts
new file mode 100644
index 0000000000..fe5e4d84a9
--- /dev/null
+++ b/src/server/api/endpoints/users/reactions.ts
@@ -0,0 +1,79 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { NoteReactions, UserProfiles } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { ApiError } from '../../error';
+
+export const meta = {
+ tags: ['users', 'reactions'],
+
+ requireCredential: false as const,
+
+ params: {
+ userId: {
+ validator: $.type(ID),
+ },
+
+ limit: {
+ validator: $.optional.num.range(1, 100),
+ default: 10,
+ },
+
+ sinceId: {
+ validator: $.optional.type(ID),
+ },
+
+ untilId: {
+ validator: $.optional.type(ID),
+ },
+
+ sinceDate: {
+ validator: $.optional.num,
+ },
+
+ untilDate: {
+ validator: $.optional.num,
+ },
+ },
+
+ res: {
+ type: 'array' as const,
+ optional: false as const, nullable: false as const,
+ items: {
+ type: 'object' as const,
+ optional: false as const, nullable: false as const,
+ ref: 'NoteReaction',
+ }
+ },
+
+ errors: {
+ reactionsNotPublic: {
+ message: 'Reactions of the user is not public.',
+ code: 'REACTIONS_NOT_PUBLIC',
+ id: '673a7dd2-6924-1093-e0c0-e68456ceae5c'
+ },
+ }
+};
+
+export default define(meta, async (ps, me) => {
+ const profile = await UserProfiles.findOneOrFail(ps.userId);
+
+ if (me == null || (me.id !== ps.userId && !profile.publicReactions)) {
+ throw new ApiError(meta.errors.reactionsNotPublic);
+ }
+
+ const query = makePaginationQuery(NoteReactions.createQueryBuilder('reaction'),
+ ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+ .andWhere(`reaction.userId = :userId`, { userId: ps.userId })
+ .leftJoinAndSelect('reaction.note', 'note');
+
+ generateVisibilityQuery(query, me);
+
+ const reactions = await query
+ .take(ps.limit!)
+ .getMany();
+
+ return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, me, { withNote: true })));
+});
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 b9fbf48fb2..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
@@ -1,6 +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'],
@@ -16,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,
@@ -44,43 +42,73 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
+ const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
+
if (ps.host) {
const q = Users.createQueryBuilder('user')
.where('user.isSuspended = FALSE')
.andWhere('user.host LIKE :host', { host: ps.host.toLowerCase() + '%' });
if (ps.username) {
- q.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' });
+ q.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' });
}
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('user.updatedAt IS NOT NULL')
- .orderBy('user.updatedAt', 'DESC')
- .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('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 });
diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts
index 8011d90b3d..9aa988d9ed 100644
--- a/src/server/api/endpoints/users/search.ts
+++ b/src/server/api/endpoints/users/search.ts
@@ -2,6 +2,7 @@ import $ from 'cafy';
import define from '../../define';
import { UserProfiles, Users } from '@/models/index';
import { User } from '@/models/entities/user';
+import { Brackets } from 'typeorm';
export const meta = {
tags: ['users'],
@@ -23,9 +24,9 @@ export const meta = {
default: 10,
},
- localOnly: {
- validator: $.optional.bool,
- default: false,
+ origin: {
+ validator: $.optional.str.or(['local', 'remote', 'combined']),
+ default: 'combined',
},
detail: {
@@ -46,63 +47,79 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
+ const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
+
const isUsername = ps.query.startsWith('@');
let users: User[] = [];
if (isUsername) {
- users = await Users.createQueryBuilder('user')
- .where('user.host IS NULL')
- .andWhere('user.isSuspended = FALSE')
- .andWhere('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' })
- .andWhere('user.updatedAt IS NOT NULL')
- .orderBy('user.updatedAt', 'DESC')
+ const usernameQuery = Users.createQueryBuilder('user')
+ .where('user.usernameLower LIKE :username', { username: ps.query.replace('@', '').toLowerCase() + '%' })
+ .andWhere(new Brackets(qb => { qb
+ .where('user.updatedAt IS NULL')
+ .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
+ }))
+ .andWhere('user.isSuspended = FALSE');
+
+ if (ps.origin === 'local') {
+ usernameQuery.andWhere('user.host IS NULL');
+ } else if (ps.origin === 'remote') {
+ usernameQuery.andWhere('user.host IS NOT NULL');
+ }
+
+ users = await usernameQuery
+ .orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.take(ps.limit!)
.skip(ps.offset)
.getMany();
+ } else {
+ const nameQuery = Users.createQueryBuilder('user')
+ .where('user.name ILIKE :query', { query: '%' + ps.query + '%' })
+ .andWhere(new Brackets(qb => { qb
+ .where('user.updatedAt IS NULL')
+ .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
+ }))
+ .andWhere('user.isSuspended = FALSE');
- if (users.length < ps.limit! && !ps.localOnly) {
- const otherUsers = await Users.createQueryBuilder('user')
- .where('user.host IS NOT NULL')
- .andWhere('user.isSuspended = FALSE')
- .andWhere('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' })
- .andWhere('user.updatedAt IS NOT NULL')
- .orderBy('user.updatedAt', 'DESC')
- .take(ps.limit! - users.length)
- .getMany();
-
- users = users.concat(otherUsers);
+ if (ps.origin === 'local') {
+ nameQuery.andWhere('user.host IS NULL');
+ } else if (ps.origin === 'remote') {
+ nameQuery.andWhere('user.host IS NOT NULL');
}
- } else {
- const profQuery = UserProfiles.createQueryBuilder('prof')
- .select('prof.userId')
- .where('prof.userHost IS NULL')
- .andWhere('prof.description ilike :query', { query: '%' + ps.query + '%' });
- users = await Users.createQueryBuilder('user')
- .where(`user.id IN (${ profQuery.getQuery() })`)
- .setParameters(profQuery.getParameters())
- .andWhere('user.updatedAt IS NOT NULL')
- .orderBy('user.updatedAt', 'DESC')
+ users = await nameQuery
+ .orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.take(ps.limit!)
.skip(ps.offset)
.getMany();
- if (users.length < ps.limit! && !ps.localOnly) {
- const profQuery2 = UserProfiles.createQueryBuilder('prof')
+ if (users.length < ps.limit!) {
+ const profQuery = UserProfiles.createQueryBuilder('prof')
.select('prof.userId')
- .where('prof.userHost IS NOT NULL')
- .andWhere('prof.description ilike :query', { query: '%' + ps.query + '%' });
+ .where('prof.description ILIKE :query', { query: '%' + ps.query + '%' });
- const otherUsers = await Users.createQueryBuilder('user')
- .where(`user.id IN (${ profQuery2.getQuery() })`)
- .setParameters(profQuery2.getParameters())
- .andWhere('user.updatedAt IS NOT NULL')
- .orderBy('user.updatedAt', 'DESC')
- .take(ps.limit! - users.length)
- .getMany();
+ if (ps.origin === 'local') {
+ profQuery.andWhere('prof.userHost IS NULL');
+ } else if (ps.origin === 'remote') {
+ profQuery.andWhere('prof.userHost IS NOT NULL');
+ }
+
+ const query = Users.createQueryBuilder('user')
+ .where(`user.id IN (${ profQuery.getQuery() })`)
+ .andWhere(new Brackets(qb => { qb
+ .where('user.updatedAt IS NULL')
+ .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
+ }))
+ .andWhere('user.isSuspended = FALSE')
+ .setParameters(profQuery.getParameters());
- users = users.concat(otherUsers);
+ users = users.concat(await query
+ .orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
+ .take(ps.limit!)
+ .skip(ps.offset)
+ .getMany()
+ );
}
}
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<User['id'], Date> = {};
private emitTypersIntervalId: ReturnType<typeof setInterval>;
@@ -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<ChannelModel['id']> = 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<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } };
+type EventUnionFromDictionary<
+ T extends object,
+ U = Events<T>
+> = U[keyof U];
+
+// name/messages(spec) pairs dictionary
+export type StreamMessages = {
+ internal: {
+ name: 'internal';
+ payload: EventUnionFromDictionary<InternalStreamTypes>;
+ };
+ broadcast: {
+ name: 'broadcast';
+ payload: EventUnionFromDictionary<BroadcastTypes>;
+ };
+ user: {
+ name: `user:${User['id']}`;
+ payload: EventUnionFromDictionary<UserStreamTypes>;
+ };
+ main: {
+ name: `mainStream:${User['id']}`;
+ payload: EventUnionFromDictionary<MainStreamTypes>;
+ };
+ drive: {
+ name: `driveStream:${User['id']}`;
+ payload: EventUnionFromDictionary<DriveStreamTypes>;
+ };
+ note: {
+ name: `noteStream:${Note['id']}`;
+ payload: EventUnionFromDictionary<NoteStreamEventTypes>;
+ };
+ channel: {
+ name: `channelStream:${Channel['id']}`;
+ payload: EventUnionFromDictionary<ChannelStreamTypes>;
+ };
+ userList: {
+ name: `userListStream:${UserList['id']}`;
+ payload: EventUnionFromDictionary<UserListStreamTypes>;
+ };
+ antenna: {
+ name: `antennaStream:${Antenna['id']}`;
+ payload: EventUnionFromDictionary<AntennaStreamTypes>;
+ };
+ messaging: {
+ name: `messagingStream:${User['id']}-${User['id']}`;
+ payload: EventUnionFromDictionary<MessagingStreamTypes>;
+ };
+ groupMessaging: {
+ name: `messagingStream:${UserGroup['id']}`;
+ payload: EventUnionFromDictionary<GroupMessagingStreamTypes>;
+ };
+ messagingIndex: {
+ name: `messagingIndexStream:${User['id']}`;
+ payload: EventUnionFromDictionary<MessagingIndexStreamTypes>;
+ };
+ reversi: {
+ name: `reversiStream:${User['id']}`;
+ payload: EventUnionFromDictionary<ReversiStreamTypes>;
+ };
+ reversiGame: {
+ name: `reversiGameStream:${ReversiGame['id']}`;
+ payload: EventUnionFromDictionary<ReversiGameStreamTypes>;
+ };
+ admin: {
+ name: `adminStream:${User['id']}`;
+ payload: EventUnionFromDictionary<AdminStreamTypes>;
+ };
+ notes: {
+ name: 'notesStream';
+ payload: Packed<'Note'>;
+ };
+};
+
+// API event definitions
+// ストリームごとのEmitterの辞書を用意
+type EventEmitterDictionary = { [x in keyof StreamMessages]: Emitter<EventEmitter, { [y in StreamMessages[x]['name']]: (e: StreamMessages[x]['payload']) => void }> };
+// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
+type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
+// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする
+export type StreamEventEmitter = UnionToIntersection<EventEmitterDictionary[keyof StreamMessages]>;
+// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる
+
+// provide stream channels union
+export type StreamChannels = StreamMessages[keyof StreamMessages]['name'];
diff --git a/src/server/web/manifest.json b/src/server/web/manifest.json
index db97531bbf..48030a2980 100644
--- a/src/server/web/manifest.json
+++ b/src/server/web/manifest.json
@@ -2,7 +2,7 @@
"short_name": "Misskey",
"name": "Misskey",
"start_url": "/",
- "display": "minimal-ui",
+ "display": "standalone",
"background_color": "#313a42",
"theme_color": "#86b300",
"icons": [