diff options
Diffstat (limited to 'src/server/api/endpoints/users')
| -rw-r--r-- | src/server/api/endpoints/users/reactions.ts | 79 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/search-by-username-and-host.ts | 74 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/search.ts | 101 |
3 files changed, 189 insertions, 65 deletions
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() + ); } } |