summaryrefslogtreecommitdiff
path: root/src/server/api/endpoints/users
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/api/endpoints/users')
-rw-r--r--src/server/api/endpoints/users/followers.ts91
-rw-r--r--src/server/api/endpoints/users/following.ts91
-rw-r--r--src/server/api/endpoints/users/get_frequently_replied_users.ts99
-rw-r--r--src/server/api/endpoints/users/notes.ts133
-rw-r--r--src/server/api/endpoints/users/recommendation.ts53
-rw-r--r--src/server/api/endpoints/users/search.ts98
-rw-r--r--src/server/api/endpoints/users/search_by_username.ts38
-rw-r--r--src/server/api/endpoints/users/show.ts56
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
+ }));
+});