diff options
Diffstat (limited to 'src/server/api/endpoints')
35 files changed, 1500 insertions, 151 deletions
diff --git a/src/server/api/endpoints/admin/accounts/create.ts b/src/server/api/endpoints/admin/accounts/create.ts new file mode 100644 index 0000000000..ac80b579b7 --- /dev/null +++ b/src/server/api/endpoints/admin/accounts/create.ts @@ -0,0 +1,33 @@ +import define from '../../../define'; +import { Users } from '../../../../../models'; +import { signup } from '../../../common/signup'; + +export const meta = { + tags: ['admin'], + + params: { + username: { + validator: Users.validateLocalUsername, + }, + + password: { + validator: Users.validatePassword, + } + } +}; + +export default define(meta, async (ps, me) => { + const noUsers = (await Users.count({})) === 0; + if (!noUsers && me == null) throw new Error('access denied'); + + const { account, secret } = await signup(ps.username, ps.password); + + const res = await Users.pack(account, account, { + detail: true, + includeSecrets: true + }); + + (res as any).token = secret; + + return res; +}); diff --git a/src/server/api/endpoints/admin/announcements/create.ts b/src/server/api/endpoints/admin/announcements/create.ts new file mode 100644 index 0000000000..c1d48a7d38 --- /dev/null +++ b/src/server/api/endpoints/admin/announcements/create.ts @@ -0,0 +1,36 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { Announcements } from '../../../../../models'; +import { genId } from '../../../../../misc/gen-id'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + params: { + title: { + validator: $.str.min(1) + }, + text: { + validator: $.str.min(1) + }, + imageUrl: { + validator: $.nullable.str.min(1) + } + } +}; + +export default define(meta, async (ps) => { + const announcement = await Announcements.save({ + id: genId(), + createdAt: new Date(), + updatedAt: null, + title: ps.title, + text: ps.text, + imageUrl: ps.imageUrl, + }); + + return announcement; +}); diff --git a/src/server/api/endpoints/admin/announcements/delete.ts b/src/server/api/endpoints/admin/announcements/delete.ts new file mode 100644 index 0000000000..284b4bf549 --- /dev/null +++ b/src/server/api/endpoints/admin/announcements/delete.ts @@ -0,0 +1,34 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { ID } from '../../../../../misc/cafy-id'; +import { Announcements } from '../../../../../models'; +import { ApiError } from '../../../error'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + params: { + id: { + validator: $.type(ID) + } + }, + + errors: { + noSuchAnnouncement: { + message: 'No such announcement.', + code: 'NO_SUCH_ANNOUNCEMENT', + id: 'ecad8040-a276-4e85-bda9-015a708d291e' + } + } +}; + +export default define(meta, async (ps, me) => { + const announcement = await Announcements.findOne(ps.id); + + if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); + + await Announcements.delete(announcement.id); +}); diff --git a/src/server/api/endpoints/admin/announcements/list.ts b/src/server/api/endpoints/admin/announcements/list.ts new file mode 100644 index 0000000000..f4e622144e --- /dev/null +++ b/src/server/api/endpoints/admin/announcements/list.ts @@ -0,0 +1,41 @@ +import $ from 'cafy'; +import { ID } from '../../../../../misc/cafy-id'; +import define from '../../../define'; +import { Announcements, AnnouncementReads } from '../../../../../models'; +import { makePaginationQuery } from '../../../common/make-pagination-query'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + params: { + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + } +}; + +export default define(meta, async (ps) => { + const query = makePaginationQuery(Announcements.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); + + const announcements = await query.take(ps.limit!).getMany(); + + for (const announcement of announcements) { + (announcement as any).reads = await AnnouncementReads.count({ + announcementId: announcement.id + }); + } + + return announcements; +}); diff --git a/src/server/api/endpoints/admin/announcements/update.ts b/src/server/api/endpoints/admin/announcements/update.ts new file mode 100644 index 0000000000..b65c3a4f93 --- /dev/null +++ b/src/server/api/endpoints/admin/announcements/update.ts @@ -0,0 +1,48 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { ID } from '../../../../../misc/cafy-id'; +import { Announcements } from '../../../../../models'; +import { ApiError } from '../../../error'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + params: { + id: { + validator: $.type(ID) + }, + title: { + validator: $.str.min(1) + }, + text: { + validator: $.str.min(1) + }, + imageUrl: { + validator: $.nullable.str.min(1) + } + }, + + errors: { + noSuchAnnouncement: { + message: 'No such announcement.', + code: 'NO_SUCH_ANNOUNCEMENT', + id: 'd3aae5a7-6372-4cb4-b61c-f511ffc2d7cc' + } + } +}; + +export default define(meta, async (ps, me) => { + const announcement = await Announcements.findOne(ps.id); + + if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); + + await Announcements.update(announcement.id, { + updatedAt: new Date(), + title: ps.title, + text: ps.text, + imageUrl: ps.imageUrl, + }); +}); diff --git a/src/server/api/endpoints/admin/emoji/list-remote.ts b/src/server/api/endpoints/admin/emoji/list-remote.ts new file mode 100644 index 0000000000..0a3e74c333 --- /dev/null +++ b/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -0,0 +1,62 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { Emojis } from '../../../../../models'; +import { toPuny } from '../../../../../misc/convert-host'; +import { makePaginationQuery } from '../../../common/make-pagination-query'; +import { ID } from '../../../../../misc/cafy-id'; + +export const meta = { + desc: { + 'ja-JP': 'カスタム絵文字を取得します。' + }, + + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + params: { + host: { + validator: $.optional.nullable.str, + default: null as any + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + } + } +}; + +export default define(meta, async (ps) => { + const q = makePaginationQuery(Emojis.createQueryBuilder('emoji'), ps.sinceId, ps.untilId); + + if (ps.host == null) { + q.andWhere(`emoji.host IS NOT NULL`); + } else { + q.andWhere(`emoji.host = :host`, { host: toPuny(ps.host) }); + } + + const emojis = await q + .orderBy('emoji.category', 'ASC') + .orderBy('emoji.name', 'ASC') + .take(ps.limit!) + .getMany(); + + return emojis.map(e => ({ + id: e.id, + name: e.name, + category: e.category, + aliases: e.aliases, + host: e.host, + url: e.url + })); +}); diff --git a/src/server/api/endpoints/admin/emoji/list.ts b/src/server/api/endpoints/admin/emoji/list.ts index d2a5e7df0d..d525a659c0 100644 --- a/src/server/api/endpoints/admin/emoji/list.ts +++ b/src/server/api/endpoints/admin/emoji/list.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; import define from '../../../define'; import { Emojis } from '../../../../../models'; -import { toPunyNullable } from '../../../../../misc/convert-host'; +import { makePaginationQuery } from '../../../common/make-pagination-query'; +import { ID } from '../../../../../misc/cafy-id'; export const meta = { desc: { @@ -14,23 +15,28 @@ export const meta = { requireModerator: true, params: { - host: { - validator: $.optional.nullable.str, - default: null as any + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), } } }; export default define(meta, async (ps) => { - const emojis = await Emojis.find({ - where: { - host: toPunyNullable(ps.host) - }, - order: { - category: 'ASC', - name: 'ASC' - } - }); + const emojis = await makePaginationQuery(Emojis.createQueryBuilder('emoji'), ps.sinceId, ps.untilId) + .andWhere(`emoji.host IS NULL`) + .orderBy('emoji.category', 'ASC') + .orderBy('emoji.name', 'ASC') + .take(ps.limit!) + .getMany(); return emojis.map(e => ({ id: e.id, diff --git a/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/src/server/api/endpoints/admin/queue/deliver-delayed.ts new file mode 100644 index 0000000000..d33837c099 --- /dev/null +++ b/src/server/api/endpoints/admin/queue/deliver-delayed.ts @@ -0,0 +1,31 @@ +import define from '../../../define'; +import { deliverQueue } from '../../../../../queue'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + params: { + } +}; + +export default define(meta, async (ps) => { + const jobs = await deliverQueue.getJobs(['delayed']); + + const res = [] as [string, number][]; + + for (const job of jobs) { + const host = new URL(job.data.to).host; + if (res.find(x => x[0] === host)) { + res.find(x => x[0] === host)![1]++; + } else { + res.push([host, 1]); + } + } + + res.sort((a, b) => b[1] - a[1]); + + return res; +}); diff --git a/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/src/server/api/endpoints/admin/queue/inbox-delayed.ts new file mode 100644 index 0000000000..643e22f10d --- /dev/null +++ b/src/server/api/endpoints/admin/queue/inbox-delayed.ts @@ -0,0 +1,31 @@ +import define from '../../../define'; +import { inboxQueue } from '../../../../../queue'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + params: { + } +}; + +export default define(meta, async (ps) => { + const jobs = await inboxQueue.getJobs(['delayed']); + + const res = [] as [string, number][]; + + for (const job of jobs) { + const host = new URL(job.data.signature.keyId).host; + if (res.find(x => x[0] === host)) { + res.find(x => x[0] === host)![1]++; + } else { + res.push([host, 1]); + } + } + + res.sort((a, b) => b[1] - a[1]); + + return res; +}); diff --git a/src/server/api/endpoints/admin/server-info.ts b/src/server/api/endpoints/admin/server-info.ts new file mode 100644 index 0000000000..f51040a2c8 --- /dev/null +++ b/src/server/api/endpoints/admin/server-info.ts @@ -0,0 +1,45 @@ +import * as os from 'os'; +import * as si from 'systeminformation'; +import { getConnection } from 'typeorm'; +import define from '../../define'; +import redis from '../../../../db/redis'; + +export const meta = { + requireCredential: false, + + desc: { + }, + + tags: ['meta'], + + params: { + }, +}; + +export default define(meta, async () => { + const memStats = await si.mem(); + const fsStats = await si.fsSize(); + const netInterface = await si.networkInterfaceDefault(); + + return { + machine: os.hostname(), + os: os.platform(), + node: process.version, + psql: await getConnection().query('SHOW server_version').then(x => x[0].server_version), + redis: redis.server_info.redis_version, + cpu: { + model: os.cpus()[0].model, + cores: os.cpus().length + }, + mem: { + total: memStats.total + }, + fs: { + total: fsStats[0].size, + used: fsStats[0].used, + }, + net: { + interface: netInterface + } + }; +}); diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts index bc37228d0a..65650f1295 100644 --- a/src/server/api/endpoints/admin/update-meta.ts +++ b/src/server/api/endpoints/admin/update-meta.ts @@ -13,16 +13,9 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireAdmin: true, params: { - announcements: { - validator: $.optional.nullable.arr($.obj()), - desc: { - 'ja-JP': 'お知らせ' - } - }, - disableRegistration: { validator: $.optional.nullable.bool, desc: { @@ -44,13 +37,6 @@ export const meta = { } }, - enableEmojiReaction: { - validator: $.optional.nullable.bool, - desc: { - 'ja-JP': '絵文字リアクションを有効にするか否か' - } - }, - useStarForReactionFallback: { validator: $.optional.nullable.bool, desc: { @@ -347,7 +333,7 @@ export const meta = { } }, - ToSUrl: { + tosUrl: { validator: $.optional.nullable.str, desc: { 'ja-JP': '利用規約のURL' @@ -413,10 +399,6 @@ export const meta = { export default define(meta, async (ps, me) => { const set = {} as Partial<Meta>; - if (ps.announcements) { - set.announcements = ps.announcements; - } - if (typeof ps.disableRegistration === 'boolean') { set.disableRegistration = ps.disableRegistration; } @@ -429,10 +411,6 @@ export default define(meta, async (ps, me) => { set.disableGlobalTimeline = ps.disableGlobalTimeline; } - if (typeof ps.enableEmojiReaction === 'boolean') { - set.enableEmojiReaction = ps.enableEmojiReaction; - } - if (typeof ps.useStarForReactionFallback === 'boolean') { set.useStarForReactionFallback = ps.useStarForReactionFallback; } @@ -601,8 +579,8 @@ export default define(meta, async (ps, me) => { set.swPrivateKey = ps.swPrivateKey; } - if (ps.ToSUrl !== undefined) { - set.ToSUrl = ps.ToSUrl; + if (ps.tosUrl !== undefined) { + set.ToSUrl = ps.tosUrl; } if (ps.repositoryUrl !== undefined) { diff --git a/src/server/api/endpoints/announcements.ts b/src/server/api/endpoints/announcements.ts new file mode 100644 index 0000000000..c6050d6092 --- /dev/null +++ b/src/server/api/endpoints/announcements.ts @@ -0,0 +1,42 @@ +import $ from 'cafy'; +import { ID } from '../../../misc/cafy-id'; +import define from '../define'; +import { Announcements, AnnouncementReads } from '../../../models'; +import { makePaginationQuery } from '../common/make-pagination-query'; + +export const meta = { + requireCredential: false, + + params: { + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + } +}; + +export default define(meta, async (ps, user) => { + const query = makePaginationQuery(Announcements.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); + + const announcements = await query.take(ps.limit!).getMany(); + + if (user) { + const reads = (await AnnouncementReads.find({ + userId: user.id + })).map(x => x.announcementId); + + for (const announcement of announcements) { + (announcement as any).isRead = reads.includes(announcement.id); + } + } + + return announcements; +}); diff --git a/src/server/api/endpoints/antennas/create.ts b/src/server/api/endpoints/antennas/create.ts new file mode 100644 index 0000000000..0e00eda1a4 --- /dev/null +++ b/src/server/api/endpoints/antennas/create.ts @@ -0,0 +1,92 @@ +import $ from 'cafy'; +import define from '../../define'; +import { genId } from '../../../../misc/gen-id'; +import { Antennas, UserLists } from '../../../../models'; +import { ID } from '../../../../misc/cafy-id'; +import { ApiError } from '../../error'; + +export const meta = { + tags: ['antennas'], + + requireCredential: true, + + kind: 'write:account', + + params: { + name: { + validator: $.str.range(1, 100) + }, + + src: { + validator: $.str.or(['home', 'all', 'users', 'list']) + }, + + userListId: { + validator: $.nullable.optional.type(ID), + }, + + keywords: { + validator: $.arr($.arr($.str)) + }, + + users: { + validator: $.arr($.str) + }, + + caseSensitive: { + validator: $.bool + }, + + withReplies: { + validator: $.bool + }, + + withFile: { + validator: $.bool + }, + + notify: { + validator: $.bool + } + }, + + errors: { + noSuchUserList: { + message: 'No such user list.', + code: 'NO_SUCH_USER_LIST', + id: '95063e93-a283-4b8b-9aa5-bcdb8df69a7f' + } + } +}; + +export default define(meta, async (ps, user) => { + let userList; + + if (ps.src === 'list') { + userList = await UserLists.findOne({ + id: ps.userListId, + userId: user.id, + }); + + if (userList == null) { + throw new ApiError(meta.errors.noSuchUserList); + } + } + + const antenna = await Antennas.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + name: ps.name, + src: ps.src, + userListId: userList ? userList.id : null, + keywords: ps.keywords, + users: ps.users, + caseSensitive: ps.caseSensitive, + withReplies: ps.withReplies, + withFile: ps.withFile, + notify: ps.notify, + }); + + return await Antennas.pack(antenna); +}); diff --git a/src/server/api/endpoints/antennas/delete.ts b/src/server/api/endpoints/antennas/delete.ts new file mode 100644 index 0000000000..6bf9165aed --- /dev/null +++ b/src/server/api/endpoints/antennas/delete.ts @@ -0,0 +1,40 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Antennas } from '../../../../models'; + +export const meta = { + tags: ['antennas'], + + requireCredential: true, + + kind: 'write:account', + + params: { + antennaId: { + validator: $.type(ID), + } + }, + + errors: { + noSuchAntenna: { + message: 'No such antenna.', + code: 'NO_SUCH_ANTENNA', + id: 'b34dcf9d-348f-44bb-99d0-6c9314cfe2df' + } + } +}; + +export default define(meta, async (ps, user) => { + const antenna = await Antennas.findOne({ + id: ps.antennaId, + userId: user.id + }); + + if (antenna == null) { + throw new ApiError(meta.errors.noSuchAntenna); + } + + await Antennas.delete(antenna.id); +}); diff --git a/src/server/api/endpoints/antennas/list.ts b/src/server/api/endpoints/antennas/list.ts new file mode 100644 index 0000000000..3f9deff32f --- /dev/null +++ b/src/server/api/endpoints/antennas/list.ts @@ -0,0 +1,18 @@ +import define from '../../define'; +import { Antennas } from '../../../../models'; + +export const meta = { + tags: ['antennas', 'account'], + + requireCredential: true, + + kind: 'read:account', +}; + +export default define(meta, async (ps, me) => { + const antennas = await Antennas.find({ + userId: me.id, + }); + + return await Promise.all(antennas.map(x => Antennas.pack(x))); +}); diff --git a/src/server/api/endpoints/antennas/notes.ts b/src/server/api/endpoints/antennas/notes.ts new file mode 100644 index 0000000000..b4c8e7e698 --- /dev/null +++ b/src/server/api/endpoints/antennas/notes.ts @@ -0,0 +1,72 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { Antennas, Notes, AntennaNotes } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { ApiError } from '../../error'; + +export const meta = { + tags: ['account', 'notes', 'antennas'], + + requireCredential: true, + + kind: 'read:account', + + params: { + antennaId: { + validator: $.type(ID), + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + }, + + errors: { + noSuchAntenna: { + message: 'No such antenna.', + code: 'NO_SUCH_ANTENNA', + id: '850926e0-fd3b-49b6-b69a-b28a5dbd82fe' + } + } +}; + +export default define(meta, async (ps, user) => { + const antenna = await Antennas.findOne({ + id: ps.antennaId, + userId: user.id + }); + + if (antenna == null) { + throw new ApiError(meta.errors.noSuchAntenna); + } + + const antennaQuery = AntennaNotes.createQueryBuilder('joining') + .select('joining.noteId') + .where('joining.antennaId = :antennaId', { antennaId: antenna.id }); + + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere(`note.id IN (${ antennaQuery.getQuery() })`) + .leftJoinAndSelect('note.user', 'user') + .setParameters(antennaQuery.getParameters()); + + generateVisibilityQuery(query, user); + generateMuteQuery(query, user); + + const notes = await query + .take(ps.limit!) + .getMany(); + + return await Notes.packMany(notes, user); +}); diff --git a/src/server/api/endpoints/antennas/show.ts b/src/server/api/endpoints/antennas/show.ts new file mode 100644 index 0000000000..dd87de1dce --- /dev/null +++ b/src/server/api/endpoints/antennas/show.ts @@ -0,0 +1,41 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Antennas } from '../../../../models'; + +export const meta = { + tags: ['antennas', 'account'], + + requireCredential: true, + + kind: 'read:account', + + params: { + antennaId: { + validator: $.type(ID), + }, + }, + + errors: { + noSuchAntenna: { + message: 'No such antenna.', + code: 'NO_SUCH_ANTENNA', + id: 'c06569fb-b025-4f23-b22d-1fcd20d2816b' + }, + } +}; + +export default define(meta, async (ps, me) => { + // Fetch the antenna + const antenna = await Antennas.findOne({ + id: ps.antennaId, + userId: me.id, + }); + + if (antenna == null) { + throw new ApiError(meta.errors.noSuchAntenna); + } + + return await Antennas.pack(antenna); +}); diff --git a/src/server/api/endpoints/antennas/update.ts b/src/server/api/endpoints/antennas/update.ts new file mode 100644 index 0000000000..28875d0f08 --- /dev/null +++ b/src/server/api/endpoints/antennas/update.ts @@ -0,0 +1,108 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Antennas, UserLists } from '../../../../models'; + +export const meta = { + tags: ['antennas'], + + requireCredential: true, + + kind: 'write:account', + + params: { + antennaId: { + validator: $.type(ID), + }, + + name: { + validator: $.str.range(1, 100) + }, + + src: { + validator: $.str.or(['home', 'all', 'users', 'list']) + }, + + userListId: { + validator: $.nullable.optional.type(ID), + }, + + keywords: { + validator: $.arr($.arr($.str)) + }, + + users: { + validator: $.arr($.str) + }, + + caseSensitive: { + validator: $.bool + }, + + withReplies: { + validator: $.bool + }, + + withFile: { + validator: $.bool + }, + + notify: { + validator: $.bool + } + }, + + errors: { + noSuchAntenna: { + message: 'No such antenna.', + code: 'NO_SUCH_ANTENNA', + id: '10c673ac-8852-48eb-aa1f-f5b67f069290' + }, + + noSuchUserList: { + message: 'No such user list.', + code: 'NO_SUCH_USER_LIST', + id: '1c6b35c9-943e-48c2-81e4-2844989407f7' + } + } +}; + +export default define(meta, async (ps, user) => { + // Fetch the antenna + const antenna = await Antennas.findOne({ + id: ps.antennaId, + userId: user.id + }); + + if (antenna == null) { + throw new ApiError(meta.errors.noSuchAntenna); + } + + let userList; + + if (ps.src === 'list') { + userList = await UserLists.findOne({ + id: ps.userListId, + userId: user.id, + }); + + if (userList == null) { + throw new ApiError(meta.errors.noSuchUserList); + } + } + + await Antennas.update(antenna.id, { + name: ps.name, + src: ps.src, + userListId: userList ? userList.id : null, + keywords: ps.keywords, + users: ps.users, + caseSensitive: ps.caseSensitive, + withReplies: ps.withReplies, + withFile: ps.withFile, + notify: ps.notify, + }); + + return await Antennas.pack(antenna.id); +}); diff --git a/src/server/api/endpoints/clips/create.ts b/src/server/api/endpoints/clips/create.ts new file mode 100644 index 0000000000..a6761c5533 --- /dev/null +++ b/src/server/api/endpoints/clips/create.ts @@ -0,0 +1,29 @@ +import $ from 'cafy'; +import define from '../../define'; +import { genId } from '../../../../misc/gen-id'; +import { Clips } from '../../../../models'; + +export const meta = { + tags: ['clips'], + + requireCredential: true, + + kind: 'write:account', + + params: { + name: { + validator: $.str.range(1, 100) + } + }, +}; + +export default define(meta, async (ps, user) => { + const clip = await Clips.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + name: ps.name, + }); + + return await Clips.pack(clip); +}); diff --git a/src/server/api/endpoints/clips/delete.ts b/src/server/api/endpoints/clips/delete.ts new file mode 100644 index 0000000000..7e185e4652 --- /dev/null +++ b/src/server/api/endpoints/clips/delete.ts @@ -0,0 +1,40 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Clips } from '../../../../models'; + +export const meta = { + tags: ['clips'], + + requireCredential: true, + + kind: 'write:account', + + params: { + clipId: { + validator: $.type(ID), + } + }, + + errors: { + noSuchClip: { + message: 'No such clip.', + code: 'NO_SUCH_CLIP', + id: '70ca08ba-6865-4630-b6fb-8494759aa754' + } + } +}; + +export default define(meta, async (ps, user) => { + const clip = await Clips.findOne({ + id: ps.clipId, + userId: user.id + }); + + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } + + await Clips.delete(clip.id); +}); diff --git a/src/server/api/endpoints/clips/list.ts b/src/server/api/endpoints/clips/list.ts new file mode 100644 index 0000000000..aa16a18d42 --- /dev/null +++ b/src/server/api/endpoints/clips/list.ts @@ -0,0 +1,18 @@ +import define from '../../define'; +import { Clips } from '../../../../models'; + +export const meta = { + tags: ['clips', 'account'], + + requireCredential: true, + + kind: 'read:account', +}; + +export default define(meta, async (ps, me) => { + const clips = await Clips.find({ + userId: me.id, + }); + + return await Promise.all(clips.map(x => Clips.pack(x))); +}); diff --git a/src/server/api/endpoints/clips/notes.ts b/src/server/api/endpoints/clips/notes.ts new file mode 100644 index 0000000000..4e76a4d1f3 --- /dev/null +++ b/src/server/api/endpoints/clips/notes.ts @@ -0,0 +1,67 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { Clips, Notes } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { generateMuteQuery } from '../../common/generate-mute-query'; + +export const meta = { + tags: ['account', 'notes', 'clips'], + + requireCredential: true, + + kind: 'read:account', + + params: { + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + }, + + errors: { + noSuchClip: { + message: 'No such list.', + code: 'NO_SUCH_CLIP', + id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00' + } + } +}; + +export default define(meta, async (ps, user) => { + const clip = await Clips.findOne({ + id: ps.clipId, + userId: user.id + }); + + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } + + const clipQuery = ClipNotes.createQueryBuilder('joining') + .select('joining.noteId') + .where('joining.clipId = :clipId', { clipId: clip.id }); + + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere(`note.id IN (${ clipQuery.getQuery() })`) + .leftJoinAndSelect('note.user', 'user') + .setParameters(clipQuery.getParameters()); + + generateVisibilityQuery(query, user); + generateMuteQuery(query, user); + + const notes = await query + .take(ps.limit!) + .getMany(); + + return await Notes.packMany(notes, user); +}); diff --git a/src/server/api/endpoints/clips/show.ts b/src/server/api/endpoints/clips/show.ts new file mode 100644 index 0000000000..0766b3e929 --- /dev/null +++ b/src/server/api/endpoints/clips/show.ts @@ -0,0 +1,41 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Clips } from '../../../../models'; + +export const meta = { + tags: ['clips', 'account'], + + requireCredential: true, + + kind: 'read:account', + + params: { + clipId: { + validator: $.type(ID), + }, + }, + + errors: { + noSuchClip: { + message: 'No such clip.', + code: 'NO_SUCH_CLIP', + id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20' + }, + } +}; + +export default define(meta, async (ps, me) => { + // Fetch the clip + const clip = await Clips.findOne({ + id: ps.clipId, + userId: me.id, + }); + + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } + + return await Clips.pack(clip); +}); diff --git a/src/server/api/endpoints/clips/update.ts b/src/server/api/endpoints/clips/update.ts new file mode 100644 index 0000000000..d1c31eb8e6 --- /dev/null +++ b/src/server/api/endpoints/clips/update.ts @@ -0,0 +1,49 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Clips } from '../../../../models'; + +export const meta = { + tags: ['clips'], + + requireCredential: true, + + kind: 'write:account', + + params: { + clipId: { + validator: $.type(ID), + }, + + name: { + validator: $.str.range(1, 100), + } + }, + + errors: { + noSuchClip: { + message: 'No such clip.', + code: 'NO_SUCH_CLIP', + id: 'b4d92d70-b216-46fa-9a3f-a8c811699257' + }, + } +}; + +export default define(meta, async (ps, user) => { + // Fetch the clip + const clip = await Clips.findOne({ + id: ps.clipId, + userId: user.id + }); + + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } + + await Clips.update(clip.id, { + name: ps.name + }); + + return await Clips.pack(clip.id); +}); diff --git a/src/server/api/endpoints/federation/followers.ts b/src/server/api/endpoints/federation/followers.ts new file mode 100644 index 0000000000..d885daf70e --- /dev/null +++ b/src/server/api/endpoints/federation/followers.ts @@ -0,0 +1,51 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { Followings } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; + +export const meta = { + tags: ['users'], + + requireCredential: false, + + params: { + host: { + validator: $.str + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + }, + + 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: 'Following', + } + }, +}; + +export default define(meta, async (ps, me) => { + const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) + .andWhere(`following.followeeHost = :host`, { host: ps.host }); + + const followings = await query + .take(ps.limit!) + .getMany(); + + return await Followings.packMany(followings, me, { populateFollowee: true }); +}); diff --git a/src/server/api/endpoints/federation/following.ts b/src/server/api/endpoints/federation/following.ts new file mode 100644 index 0000000000..1f79817318 --- /dev/null +++ b/src/server/api/endpoints/federation/following.ts @@ -0,0 +1,51 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { Followings } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; + +export const meta = { + tags: ['users'], + + requireCredential: false, + + params: { + host: { + validator: $.str + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + }, + + 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: 'Following', + } + }, +}; + +export default define(meta, async (ps, me) => { + const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) + .andWhere(`following.followerHost = :host`, { host: ps.host }); + + const followings = await query + .take(ps.limit!) + .getMany(); + + return await Followings.packMany(followings, me, { populateFollowee: true }); +}); diff --git a/src/server/api/endpoints/federation/instances.ts b/src/server/api/endpoints/federation/instances.ts index bc0eb9a1d7..002cfd4335 100644 --- a/src/server/api/endpoints/federation/instances.ts +++ b/src/server/api/endpoints/federation/instances.ts @@ -9,6 +9,10 @@ export const meta = { requireCredential: false, params: { + host: { + validator: $.optional.nullable.str, + }, + blocked: { validator: $.optional.nullable.bool, }, @@ -17,7 +21,19 @@ export const meta = { validator: $.optional.nullable.bool, }, - markedAsClosed: { + suspended: { + validator: $.optional.nullable.bool, + }, + + federating: { + validator: $.optional.nullable.bool, + }, + + subscribing: { + validator: $.optional.nullable.bool, + }, + + publishing: { validator: $.optional.nullable.bool, }, @@ -41,6 +57,8 @@ export default define(meta, async (ps, me) => { const query = Instances.createQueryBuilder('instance'); switch (ps.sort) { + case '+pubSub': query.orderBy('instance.followingCount', 'DESC').orderBy('instance.followersCount', 'DESC'); break; + case '-pubSub': query.orderBy('instance.followingCount', 'ASC').orderBy('instance.followersCount', 'ASC'); break; case '+notes': query.orderBy('instance.notesCount', 'DESC'); break; case '-notes': query.orderBy('instance.notesCount', 'ASC'); break; case '+users': query.orderBy('instance.usersCount', 'DESC'); break; @@ -78,14 +96,42 @@ export default define(meta, async (ps, me) => { } } - if (typeof ps.markedAsClosed === 'boolean') { - if (ps.markedAsClosed) { - query.andWhere('instance.isMarkedAsClosed = TRUE'); + if (typeof ps.suspended === 'boolean') { + if (ps.suspended) { + query.andWhere('instance.isSuspended = TRUE'); } else { - query.andWhere('instance.isMarkedAsClosed = FALSE'); + query.andWhere('instance.isSuspended = FALSE'); } } + if (typeof ps.federating === 'boolean') { + if (ps.federating) { + query.andWhere('((instance.followingCount > 0) OR (instance.followersCount > 0))'); + } else { + query.andWhere('((instance.followingCount = 0) AND (instance.followersCount = 0))'); + } + } + + if (typeof ps.subscribing === 'boolean') { + if (ps.subscribing) { + query.andWhere('instance.followersCount > 0'); + } else { + query.andWhere('instance.followersCount = 0'); + } + } + + if (typeof ps.publishing === 'boolean') { + if (ps.publishing) { + query.andWhere('instance.followingCount > 0'); + } else { + query.andWhere('instance.followingCount = 0'); + } + } + + if (ps.host) { + query.andWhere('instance.host like :host', { host: '%' + ps.host.toLowerCase() + '%' }) + } + const instances = await query.take(ps.limit!).skip(ps.offset).getMany(); return instances; diff --git a/src/server/api/endpoints/federation/users.ts b/src/server/api/endpoints/federation/users.ts new file mode 100644 index 0000000000..f69bbf949c --- /dev/null +++ b/src/server/api/endpoints/federation/users.ts @@ -0,0 +1,51 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { Users } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; + +export const meta = { + tags: ['users'], + + requireCredential: false, + + params: { + host: { + validator: $.str + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + }, + + 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: 'User', + } + }, +}; + +export default define(meta, async (ps, me) => { + const query = makePaginationQuery(Users.createQueryBuilder('user'), ps.sinceId, ps.untilId) + .andWhere(`user.host = :host`, { host: ps.host }); + + const users = await query + .take(ps.limit!) + .getMany(); + + return await Users.packMany(users, me, { detail: true }); +}); diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts index cd00501a2e..f624550d49 100644 --- a/src/server/api/endpoints/i/notifications.ts +++ b/src/server/api/endpoints/i/notifications.ts @@ -42,12 +42,12 @@ export const meta = { }, includeTypes: { - validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'])), + validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted'])), default: [] as string[] }, excludeTypes: { - validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'])), + validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted'])), default: [] as string[] } }, diff --git a/src/server/api/endpoints/i/read-announcement.ts b/src/server/api/endpoints/i/read-announcement.ts new file mode 100644 index 0000000000..c5fbe7d576 --- /dev/null +++ b/src/server/api/endpoints/i/read-announcement.ts @@ -0,0 +1,60 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { genId } from '../../../../misc/gen-id'; +import { AnnouncementReads, Announcements, Users } from '../../../../models'; +import { publishMainStream } from '../../../../services/stream'; + +export const meta = { + tags: ['account'], + + requireCredential: true, + + kind: 'write:account', + + params: { + announcementId: { + validator: $.type(ID), + }, + }, + + errors: { + noSuchAnnouncement: { + message: 'No such announcement.', + code: 'NO_SUCH_ANNOUNCEMENT', + id: '184663db-df88-4bc2-8b52-fb85f0681939' + }, + } +}; + +export default define(meta, async (ps, user) => { + // Check if announcement exists + const announcement = await Announcements.findOne(ps.announcementId); + + if (announcement == null) { + throw new ApiError(meta.errors.noSuchAnnouncement); + } + + // Check if already read + const read = await AnnouncementReads.findOne({ + announcementId: ps.announcementId, + userId: user.id + }); + + if (read != null) { + return; + } + + // Create read + await AnnouncementReads.save({ + id: genId(), + createdAt: new Date(), + announcementId: ps.announcementId, + userId: user.id, + }); + + if (!await Users.getHasUnreadAnnouncement(user.id)) { + publishMainStream(user.id, 'readAllAnnouncements'); + } +}); diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index b71c35946e..2c605a6f0b 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -1,11 +1,8 @@ import $ from 'cafy'; -import * as os from 'os'; import config from '../../../config'; import define from '../define'; import { fetchMeta } from '../../../misc/fetch-meta'; -import { Emojis } from '../../../models'; -import { getConnection } from 'typeorm'; -import redis from '../../../db/redis'; +import { Emojis, Users } from '../../../models'; import { DB_MAX_NOTE_TEXT_LENGTH } from '../../../misc/hard-limits'; export const meta = { @@ -83,11 +80,6 @@ export const meta = { optional: false as const, nullable: false as const, description: 'Whether disabled GTL.', }, - enableEmojiReaction: { - type: 'boolean' as const, - optional: false as const, nullable: false as const, - description: 'Whether enabled emoji reaction.', - }, } } }; @@ -119,27 +111,15 @@ export default define(meta, async (ps, me) => { uri: config.url, description: instance.description, langs: instance.langs, - ToSUrl: instance.ToSUrl, + tosUrl: instance.ToSUrl, repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, secure: config.https != null, - machine: os.hostname(), - os: os.platform(), - node: process.version, - psql: await getConnection().query('SHOW server_version').then(x => x[0].server_version), - redis: redis.server_info.redis_version, - - cpu: { - model: os.cpus()[0].model, - cores: os.cpus().length - }, - announcements: instance.announcements || [], disableRegistration: instance.disableRegistration, disableLocalTimeline: instance.disableLocalTimeline, disableGlobalTimeline: instance.disableGlobalTimeline, - enableEmojiReaction: instance.enableEmojiReaction, driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, cacheRemoteFiles: instance.cacheRemoteFiles, @@ -159,6 +139,7 @@ export default define(meta, async (ps, me) => { category: e.category, url: e.url, })), + requireSetup: (await Users.count({})) === 0, enableEmail: instance.enableEmail, enableTwitterIntegration: instance.enableTwitterIntegration, @@ -183,7 +164,7 @@ export default define(meta, async (ps, me) => { }; } - if (me && (me.isAdmin || me.isModerator)) { + if (me && me.isAdmin) { response.useStarForReactionFallback = instance.useStarForReactionFallback; response.pinnedUsers = instance.pinnedUsers; response.hiddenTags = instance.hiddenTags; diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts index 810ad51b67..73db73ed97 100644 --- a/src/server/api/endpoints/notes/create.ts +++ b/src/server/api/endpoints/notes/create.ts @@ -113,23 +113,6 @@ export const meta = { } }, - geo: { - validator: $.optional.nullable.obj({ - coordinates: $.arr().length(2) - .item(0, $.num.range(-180, 180)) - .item(1, $.num.range(-90, 90)), - altitude: $.nullable.num, - accuracy: $.nullable.num, - altitudeAccuracy: $.nullable.num, - heading: $.nullable.num.range(0, 360), - speed: $.nullable.num - }).strict(), - desc: { - 'ja-JP': '位置情報' - }, - ref: 'geo' - }, - fileIds: { validator: $.optional.arr($.type(ID)).unique().range(1, 4), desc: { @@ -308,7 +291,6 @@ export default define(meta, async (ps, user, app) => { apMentions: ps.noExtractMentions ? [] : undefined, apHashtags: ps.noExtractHashtags ? [] : undefined, apEmojis: ps.noExtractEmojis ? [] : undefined, - geo: ps.geo }); return { diff --git a/src/server/api/endpoints/notes/featured.ts b/src/server/api/endpoints/notes/featured.ts index 0a1d8668b0..a499afabf0 100644 --- a/src/server/api/endpoints/notes/featured.ts +++ b/src/server/api/endpoints/notes/featured.ts @@ -15,12 +15,17 @@ export const meta = { params: { limit: { - validator: $.optional.num.range(1, 30), + validator: $.optional.num.range(1, 100), default: 10, desc: { 'ja-JP': '最大数' } - } + }, + + offset: { + validator: $.optional.num.min(0), + default: 0 + }, }, res: { @@ -35,6 +40,7 @@ export const meta = { }; export default define(meta, async (ps, user) => { + const max = 30; const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで const query = Notes.createQueryBuilder('note') @@ -46,7 +52,14 @@ export default define(meta, async (ps, user) => { if (user) generateMuteQuery(query, user); - const notes = await query.orderBy('note.score', 'DESC').take(ps.limit!).getMany(); + let notes = await query + .orderBy('note.score', 'DESC') + .take(max) + .getMany(); + + notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + + notes = notes.slice(ps.offset, ps.offset + ps.limit); return await Notes.packMany(notes, user); }); diff --git a/src/server/api/endpoints/notes/search.ts b/src/server/api/endpoints/notes/search.ts index 5557b469e4..efc08d0d4a 100644 --- a/src/server/api/endpoints/notes/search.ts +++ b/src/server/api/endpoints/notes/search.ts @@ -1,11 +1,13 @@ import $ from 'cafy'; import es from '../../../../db/elasticsearch'; import define from '../../define'; -import { ApiError } from '../../error'; import { Notes } from '../../../../models'; import { In } from 'typeorm'; import { ID } from '../../../../misc/cafy-id'; import config from '../../../../config'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { generateMuteQuery } from '../../common/generate-mute-query'; export const meta = { desc: { @@ -22,16 +24,19 @@ export const meta = { validator: $.str }, + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + limit: { validator: $.optional.num.range(1, 100), default: 10 }, - offset: { - validator: $.optional.num.min(0), - default: 0 - }, - host: { validator: $.optional.nullable.str, default: undefined @@ -54,74 +59,80 @@ export const meta = { }, errors: { - searchingNotAvailable: { - message: 'Searching not available.', - code: 'SEARCHING_NOT_AVAILABLE', - id: '7ee9c119-16a1-479f-a6fd-6fab00ed946f' - } } }; export default define(meta, async (ps, me) => { - if (es == null) throw new ApiError(meta.errors.searchingNotAvailable); + if (es == null) { + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.text ILIKE :q', { q: `%${ps.query}%` }) + .leftJoinAndSelect('note.user', 'user'); - const userQuery = ps.userId != null ? [{ - term: { - userId: ps.userId - } - }] : []; + generateVisibilityQuery(query, me); + if (me) generateMuteQuery(query, me); - const hostQuery = ps.userId == null ? - ps.host === null ? [{ - bool: { - must_not: { - exists: { - field: 'userHost' - } - } - } - }] : ps.host !== undefined ? [{ + const notes = await query.take(ps.limit!).getMany(); + + return await Notes.packMany(notes, me); + } else { + const userQuery = ps.userId != null ? [{ term: { - userHost: ps.host + userId: ps.userId } - }] : [] - : []; + }] : []; - const result = await es.search({ - index: config.elasticsearch.index || 'misskey_note', - body: { - size: ps.limit!, - from: ps.offset, - query: { + const hostQuery = ps.userId == null ? + ps.host === null ? [{ bool: { - must: [{ - simple_query_string: { - fields: ['text'], - query: ps.query.toLowerCase(), - default_operator: 'and' - }, - }, ...hostQuery, ...userQuery] + must_not: { + exists: { + field: 'userHost' + } + } } - }, - sort: [{ - _doc: 'desc' - }] - } - }); + }] : ps.host !== undefined ? [{ + term: { + userHost: ps.host + } + }] : [] + : []; + + const result = await es.search({ + index: config.elasticsearch.index || 'misskey_note', + body: { + size: ps.limit!, + from: ps.offset, + query: { + bool: { + must: [{ + simple_query_string: { + fields: ['text'], + query: ps.query.toLowerCase(), + default_operator: 'and' + }, + }, ...hostQuery, ...userQuery] + } + }, + sort: [{ + _doc: 'desc' + }] + } + }); - const hits = result.body.hits.hits.map((hit: any) => hit._id); + const hits = result.body.hits.hits.map((hit: any) => hit._id); - if (hits.length === 0) return []; + if (hits.length === 0) return []; - // Fetch found notes - const notes = await Notes.find({ - where: { - id: In(hits) - }, - order: { - id: -1 - } - }); + // Fetch found notes + const notes = await Notes.find({ + where: { + id: In(hits) + }, + order: { + id: -1 + } + }); - return await Notes.packMany(notes, me); + return await Notes.packMany(notes, me); + } }); 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 new file mode 100644 index 0000000000..8544731dfd --- /dev/null +++ b/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -0,0 +1,101 @@ +import $ from 'cafy'; +import define from '../../define'; +import { Users } from '../../../../models'; +import { User } from '../../../../models/entities/user'; + +export const meta = { + desc: { + 'ja-JP': 'ユーザーを検索します。' + }, + + tags: ['users'], + + requireCredential: false, + + params: { + username: { + validator: $.optional.nullable.str, + desc: { + 'ja-JP': 'クエリ' + } + }, + + host: { + validator: $.optional.nullable.str, + desc: { + 'ja-JP': 'クエリ' + } + }, + + offset: { + validator: $.optional.num.min(0), + default: 0, + desc: { + 'ja-JP': 'オフセット' + } + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10, + desc: { + 'ja-JP': '取得する数' + } + }, + + detail: { + validator: $.optional.bool, + default: true, + desc: { + 'ja-JP': '詳細なユーザー情報を含めるか否か' + } + }, + }, + + 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: 'User', + } + }, +}; + +export default define(meta, async (ps, me) => { + 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() + '%' }) + } + + const users = await q.take(ps.limit!).skip(ps.offset).getMany(); + + return await Users.packMany(users, me, { detail: ps.detail }); + } else { + let users = await Users.createQueryBuilder('user') + .where('user.host IS NULL') + .andWhere('user.isSuspended = FALSE') + .andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' }) + .take(ps.limit!) + .skip(ps.offset) + .getMany(); + + if (users.length < ps.limit!) { + const otherUsers = await Users.createQueryBuilder('user') + .where('user.host IS NOT NULL') + .andWhere('user.isSuspended = FALSE') + .andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' }) + .take(ps.limit! - users.length) + .getMany(); + + users = users.concat(otherUsers); + } + + return await Users.packMany(users, me, { detail: ps.detail }); + } +}); |