diff options
| author | tamaina <tamaina@hotmail.co.jp> | 2018-04-11 20:27:09 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-04-11 20:27:09 +0900 |
| commit | d43fe853c3605696e2e57e240845d0fc9c284f61 (patch) | |
| tree | 838914e262c0fca5737588a7bba64e2b9f3d8e5f /src/server/api/endpoints/users | |
| parent | Update README.md (diff) | |
| parent | wip #1443 (diff) | |
| download | misskey-d43fe853c3605696e2e57e240845d0fc9c284f61.tar.gz misskey-d43fe853c3605696e2e57e240845d0fc9c284f61.tar.bz2 misskey-d43fe853c3605696e2e57e240845d0fc9c284f61.zip | |
Merge pull request #1 from syuilo/master
追従
Diffstat (limited to 'src/server/api/endpoints/users')
| -rw-r--r-- | src/server/api/endpoints/users/followers.ts | 91 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/following.ts | 91 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/get_frequently_replied_users.ts | 99 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/notes.ts | 133 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/recommendation.ts | 53 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/search.ts | 98 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/search_by_username.ts | 38 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/show.ts | 56 |
8 files changed, 659 insertions, 0 deletions
diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts new file mode 100644 index 0000000000..0222313e81 --- /dev/null +++ b/src/server/api/endpoints/users/followers.ts @@ -0,0 +1,91 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../../../models/user'; +import Following from '../../../../models/following'; +import { pack } from '../../../../models/user'; +import getFriends from '../../common/get-friends'; + +/** + * Get followers of a user + * + * @param {any} params + * @param {any} me + * @return {Promise<any>} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); + + // Get 'iknow' parameter + const [iknow = false, iknowErr] = $(params.iknow).optional.boolean().$; + if (iknowErr) return rej('invalid iknow param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'cursor' parameter + const [cursor = null, cursorErr] = $(params.cursor).optional.id().$; + if (cursorErr) return rej('invalid cursor param'); + + // Lookup user + const user = await User.findOne({ + _id: userId + }, { + fields: { + _id: true + } + }); + + if (user === null) { + return rej('user not found'); + } + + // Construct query + const query = { + followeeId: user._id + } as any; + + // ログインしていてかつ iknow フラグがあるとき + if (me && iknow) { + // Get my friends + const myFriends = await getFriends(me._id); + + query.followerId = { + $in: myFriends + }; + } + + // カーソルが指定されている場合 + if (cursor) { + query._id = { + $lt: cursor + }; + } + + // Get followers + const following = await Following + .find(query, { + limit: limit + 1, + sort: { _id: -1 } + }); + + // 「次のページ」があるかどうか + const inStock = following.length === limit + 1; + if (inStock) { + following.pop(); + } + + // Serialize + const users = await Promise.all(following.map(async f => + await pack(f.followerId, me, { detail: true }))); + + // Response + res({ + users: users, + next: inStock ? following[following.length - 1]._id : null, + }); +}); diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts new file mode 100644 index 0000000000..2372f57fbe --- /dev/null +++ b/src/server/api/endpoints/users/following.ts @@ -0,0 +1,91 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../../../models/user'; +import Following from '../../../../models/following'; +import { pack } from '../../../../models/user'; +import getFriends from '../../common/get-friends'; + +/** + * Get following users of a user + * + * @param {any} params + * @param {any} me + * @return {Promise<any>} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); + + // Get 'iknow' parameter + const [iknow = false, iknowErr] = $(params.iknow).optional.boolean().$; + if (iknowErr) return rej('invalid iknow param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'cursor' parameter + const [cursor = null, cursorErr] = $(params.cursor).optional.id().$; + if (cursorErr) return rej('invalid cursor param'); + + // Lookup user + const user = await User.findOne({ + _id: userId + }, { + fields: { + _id: true + } + }); + + if (user === null) { + return rej('user not found'); + } + + // Construct query + const query = { + followerId: user._id + } as any; + + // ログインしていてかつ iknow フラグがあるとき + if (me && iknow) { + // Get my friends + const myFriends = await getFriends(me._id); + + query.followeeId = { + $in: myFriends + }; + } + + // カーソルが指定されている場合 + if (cursor) { + query._id = { + $lt: cursor + }; + } + + // Get followers + const following = await Following + .find(query, { + limit: limit + 1, + sort: { _id: -1 } + }); + + // 「次のページ」があるかどうか + const inStock = following.length === limit + 1; + if (inStock) { + following.pop(); + } + + // Serialize + const users = await Promise.all(following.map(async f => + await pack(f.followeeId, me, { detail: true }))); + + // Response + res({ + users: users, + next: inStock ? following[following.length - 1]._id : null, + }); +}); diff --git a/src/server/api/endpoints/users/get_frequently_replied_users.ts b/src/server/api/endpoints/users/get_frequently_replied_users.ts new file mode 100644 index 0000000000..7a98f44e98 --- /dev/null +++ b/src/server/api/endpoints/users/get_frequently_replied_users.ts @@ -0,0 +1,99 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Note from '../../../../models/note'; +import User, { pack } from '../../../../models/user'; + +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Lookup user + const user = await User.findOne({ + _id: userId + }, { + fields: { + _id: true + } + }); + + if (user === null) { + return rej('user not found'); + } + + // Fetch recent notes + const recentNotes = await Note.find({ + userId: user._id, + replyId: { + $exists: true, + $ne: null + } + }, { + sort: { + _id: -1 + }, + limit: 1000, + fields: { + _id: false, + replyId: true + } + }); + + // 投稿が少なかったら中断 + if (recentNotes.length === 0) { + return res([]); + } + + const replyTargetNotes = await Note.find({ + _id: { + $in: recentNotes.map(p => p.replyId) + }, + userId: { + $ne: user._id + } + }, { + fields: { + _id: false, + userId: true + } + }); + + const repliedUsers = {}; + + // Extract replies from recent notes + replyTargetNotes.forEach(note => { + const userId = note.userId.toString(); + if (repliedUsers[userId]) { + repliedUsers[userId]++; + } else { + repliedUsers[userId] = 1; + } + }); + + // Calc peak + let peak = 0; + Object.keys(repliedUsers).forEach(user => { + if (repliedUsers[user] > peak) peak = repliedUsers[user]; + }); + + // Sort replies by frequency + const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]); + + // Extract top replied users + const topRepliedUsers = repliedUsersSorted.slice(0, limit); + + // Make replies object (includes weights) + const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({ + user: await pack(user, me, { detail: true }), + weight: repliedUsers[user] / peak + }))); + + // Response + res(repliesObj); +}); diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts new file mode 100644 index 0000000000..e91b75e1d3 --- /dev/null +++ b/src/server/api/endpoints/users/notes.ts @@ -0,0 +1,133 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import getHostLower from '../../common/get-host-lower'; +import Note, { pack } from '../../../../models/note'; +import User from '../../../../models/user'; + +/** + * Get notes of a user + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).optional.id().$; + if (userIdErr) return rej('invalid userId param'); + + // Get 'username' parameter + const [username, usernameErr] = $(params.username).optional.string().$; + if (usernameErr) return rej('invalid username param'); + + if (userId === undefined && username === undefined) { + return rej('userId or pair of username and host is required'); + } + + // Get 'host' parameter + const [host, hostErr] = $(params.host).optional.string().$; + if (hostErr) return rej('invalid host param'); + + if (userId === undefined && host === undefined) { + return rej('userId or pair of username and host is required'); + } + + // Get 'includeReplies' parameter + const [includeReplies = true, includeRepliesErr] = $(params.includeReplies).optional.boolean().$; + if (includeRepliesErr) return rej('invalid includeReplies param'); + + // Get 'withMedia' parameter + const [withMedia = false, withMediaErr] = $(params.withMedia).optional.boolean().$; + if (withMediaErr) return rej('invalid withMedia param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); + + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); + + // Get 'sinceDate' parameter + const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$; + if (sinceDateErr) throw 'invalid sinceDate param'; + + // Get 'untilDate' parameter + const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$; + if (untilDateErr) throw 'invalid untilDate param'; + + // Check if only one of sinceId, untilId, sinceDate, untilDate specified + if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) { + throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; + } + + const q = userId !== undefined + ? { _id: userId } + : { usernameLower: username.toLowerCase(), hostLower: getHostLower(host) } ; + + // Lookup user + const user = await User.findOne(q, { + fields: { + _id: true + } + }); + + if (user === null) { + return rej('user not found'); + } + + //#region Construct query + const sort = { + _id: -1 + }; + + const query = { + userId: user._id + } as any; + + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } else if (sinceDate) { + sort._id = 1; + query.createdAt = { + $gt: new Date(sinceDate) + }; + } else if (untilDate) { + query.createdAt = { + $lt: new Date(untilDate) + }; + } + + if (!includeReplies) { + query.replyId = null; + } + + if (withMedia) { + query.mediaIds = { + $exists: true, + $ne: [] + }; + } + //#endregion + + // Issue query + const notes = await Note + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(notes.map(async (note) => + await pack(note, me) + ))); +}); diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts new file mode 100644 index 0000000000..2de22da13e --- /dev/null +++ b/src/server/api/endpoints/users/recommendation.ts @@ -0,0 +1,53 @@ +/** + * Module dependencies + */ +const ms = require('ms'); +import $ from 'cafy'; +import User, { pack } from '../../../../models/user'; +import getFriends from '../../common/get-friends'; + +/** + * Get recommended users + * + * @param {any} params + * @param {any} me + * @return {Promise<any>} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // ID list of the user itself and other users who the user follows + const followingIds = await getFriends(me._id); + + const users = await User + .find({ + _id: { + $nin: followingIds + }, + $or: [ + { + 'lastUsedAt': { + $gte: new Date(Date.now() - ms('7days')) + } + }, { + host: { $ne: null } + } + ] + }, { + limit: limit, + skip: offset, + sort: { + followersCount: -1 + } + }); + + // Serialize + res(await Promise.all(users.map(async user => + await pack(user, me, { detail: true })))); +}); diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts new file mode 100644 index 0000000000..da30f47c2a --- /dev/null +++ b/src/server/api/endpoints/users/search.ts @@ -0,0 +1,98 @@ +/** + * Module dependencies + */ +import * as mongo from 'mongodb'; +import $ from 'cafy'; +import User, { pack } from '../../../../models/user'; +import config from '../../../../config'; +const escapeRegexp = require('escape-regexp'); + +/** + * Search a user + * + * @param {any} params + * @param {any} me + * @return {Promise<any>} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'query' parameter + const [query, queryError] = $(params.query).string().pipe(x => x != '').$; + if (queryError) return rej('invalid query param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Get 'max' parameter + const [max = 10, maxErr] = $(params.max).optional.number().range(1, 30).$; + if (maxErr) return rej('invalid max param'); + + // If Elasticsearch is available, search by $ + // If not, search by MongoDB + (config.elasticsearch.enable ? byElasticsearch : byNative) + (res, rej, me, query, offset, max); +}); + +// Search by MongoDB +async function byNative(res, rej, me, query, offset, max) { + const escapedQuery = escapeRegexp(query); + + // Search users + const users = await User + .find({ + $or: [{ + usernameLower: new RegExp(escapedQuery.replace('@', '').toLowerCase()) + }, { + name: new RegExp(escapedQuery) + }] + }, { + limit: max + }); + + // Serialize + res(await Promise.all(users.map(async user => + await pack(user, me, { detail: true })))); +} + +// Search by Elasticsearch +async function byElasticsearch(res, rej, me, query, offset, max) { + const es = require('../../db/elasticsearch'); + + es.search({ + index: 'misskey', + type: 'user', + body: { + size: max, + from: offset, + query: { + simple_query_string: { + fields: ['username', 'name', 'bio'], + query: query, + default_operator: 'and' + } + } + } + }, async (error, response) => { + if (error) { + console.error(error); + return res(500); + } + + if (response.hits.total === 0) { + return res([]); + } + + const hits = response.hits.hits.map(hit => new mongo.ObjectID(hit._id)); + + const users = await User + .find({ + _id: { + $in: hits + } + }); + + // Serialize + res(await Promise.all(users.map(async user => + await pack(user, me, { detail: true })))); + }); +} diff --git a/src/server/api/endpoints/users/search_by_username.ts b/src/server/api/endpoints/users/search_by_username.ts new file mode 100644 index 0000000000..5f6ececff9 --- /dev/null +++ b/src/server/api/endpoints/users/search_by_username.ts @@ -0,0 +1,38 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User, { pack } from '../../../../models/user'; + +/** + * Search a user by username + * + * @param {any} params + * @param {any} me + * @return {Promise<any>} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'query' parameter + const [query, queryError] = $(params.query).string().$; + if (queryError) return rej('invalid query param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + const users = await User + .find({ + usernameLower: new RegExp(query.toLowerCase()) + }, { + limit: limit, + skip: offset + }); + + // Serialize + res(await Promise.all(users.map(async user => + await pack(user, me, { detail: true })))); +}); diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts new file mode 100644 index 0000000000..7e7f5dc488 --- /dev/null +++ b/src/server/api/endpoints/users/show.ts @@ -0,0 +1,56 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User, { pack } from '../../../../models/user'; +import resolveRemoteUser from '../../../../remote/resolve-user'; + +const cursorOption = { fields: { data: false } }; + +/** + * Show a user + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + let user; + + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).optional.id().$; + if (userIdErr) return rej('invalid userId param'); + + // Get 'username' parameter + const [username, usernameErr] = $(params.username).optional.string().$; + if (usernameErr) return rej('invalid username param'); + + // Get 'host' parameter + const [host, hostErr] = $(params.host).nullable.optional.string().$; + if (hostErr) return rej('invalid host param'); + + if (userId === undefined && typeof username !== 'string') { + return rej('userId or pair of username and host is required'); + } + + // Lookup user + if (typeof host === 'string') { + try { + user = await resolveRemoteUser(username, host, cursorOption); + } catch (e) { + console.warn(`failed to resolve remote user: ${e}`); + return rej('failed to resolve remote user'); + } + } else { + const q = userId !== undefined + ? { _id: userId } + : { usernameLower: username.toLowerCase(), host: null }; + + user = await User.findOne(q, cursorOption); + + if (user === null) { + return rej('user not found'); + } + } + + // Send response + res(await pack(user, me, { + detail: true + })); +}); |