From 0d5bc3be6611a6877a71233ac57c0987027c0398 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 19 Apr 2018 12:43:25 +0900 Subject: [PATCH] =?UTF-8?q?=E3=82=B9=E3=83=88=E3=83=BC=E3=82=AD=E3=83=B3?= =?UTF-8?q?=E3=82=B0=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1511 --- locales/en.yml | 3 + locales/fr.yml | 3 + locales/ja.yml | 3 + migration/2018-04-19.js | 49 +++++++++++++++ .../desktop/views/pages/user/user.profile.vue | 30 +++++++++- src/models/following.ts | 11 ++++ src/models/note.ts | 5 +- src/models/user.ts | 43 +++++++------ src/server/api/common/get-friends.ts | 29 ++++++++- src/server/api/endpoints.ts | 18 ++++++ src/server/api/endpoints/following/stalk.ts | 36 +++++++++++ src/server/api/endpoints/following/unstalk.ts | 35 +++++++++++ src/server/api/endpoints/i/notifications.ts | 4 +- src/server/api/endpoints/mute/list.ts | 4 +- src/server/api/endpoints/notes/mentions.ts | 4 +- src/server/api/endpoints/notes/search.ts | 4 +- src/server/api/endpoints/notes/timeline.ts | 60 +++++++++++++------ src/server/api/endpoints/users/followers.ts | 4 +- src/server/api/endpoints/users/following.ts | 4 +- .../api/endpoints/users/recommendation.ts | 4 +- src/services/following/create.ts | 13 +++- src/services/note/create.ts | 24 +++----- 22 files changed, 310 insertions(+), 80 deletions(-) create mode 100644 migration/2018-04-19.js create mode 100644 src/server/api/endpoints/following/stalk.ts create mode 100644 src/server/api/endpoints/following/unstalk.ts diff --git a/locales/en.yml b/locales/en.yml index 9388aedbf8..4eb0a3446c 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -419,6 +419,9 @@ desktop/views/pages/user/user.photos.vue: desktop/views/pages/user/user.profile.vue: follows-you: "Follows you" + stalk: "Stalk" + stalking: "Stalking" + unstalk: "Unstalk" mute: "Mute" muted: "Muting" unmute: "Unmute" diff --git a/locales/fr.yml b/locales/fr.yml index cd756194ab..0e0019e565 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -419,6 +419,9 @@ desktop/views/pages/user/user.photos.vue: desktop/views/pages/user/user.profile.vue: follows-you: "Vous suis" + stalk: "ストークする" + stalking: "ストーキングしています" + unstalk: "ストーク解除" mute: "Mettre en sourdine" muted: "Muting" unmute: "Enlever la sourdine" diff --git a/locales/ja.yml b/locales/ja.yml index 96b4a1d0ea..ed32deae57 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -419,6 +419,9 @@ desktop/views/pages/user/user.photos.vue: desktop/views/pages/user/user.profile.vue: follows-you: "フォローされています" + stalk: "ストークする" + stalking: "ストーキングしています" + unstalk: "ストーク解除" mute: "ミュートする" muted: "ミュートしています" unmute: "ミュート解除" diff --git a/migration/2018-04-19.js b/migration/2018-04-19.js new file mode 100644 index 0000000000..b0df22c009 --- /dev/null +++ b/migration/2018-04-19.js @@ -0,0 +1,49 @@ +// for Node.js interpret + +const { default: User } = require('../built/models/user'); +const { default: Following } = require('../built/models/following'); +const { default: zip } = require('@prezzemolo/zip') + +const migrate = async (following) => { + const follower = await User.findOne({ _id: following.followerId }); + const followee = await User.findOne({ _id: following.followeeId }); + const result = await Following.update(following._id, { + $set: { + stalk: true, + _follower: { + host: follower.host, + inbox: follower.host != null ? follower.inbox : undefined + }, + _followee: { + host: followee.host, + inbox: followee.host != null ? followee.inbox : undefined + } + } + }); + return result.ok === 1; +} + +async function main() { + const count = await Following.count({}); + + const dop = Number.parseInt(process.argv[2]) || 5 + const idop = ((count - (count % dop)) / dop) + 1 + + return zip( + 1, + async (time) => { + console.log(`${time} / ${idop}`) + const doc = await Following.find({}, { + limit: dop, skip: time * dop + }) + return Promise.all(doc.map(migrate)) + }, + idop + ).then(a => { + const rv = [] + a.forEach(e => rv.push(...e)) + return rv + }) +} + +main().then(console.dir).catch(console.error) diff --git a/src/client/app/desktop/views/pages/user/user.profile.vue b/src/client/app/desktop/views/pages/user/user.profile.vue index 72750e1b3d..774f300a38 100644 --- a/src/client/app/desktop/views/pages/user/user.profile.vue +++ b/src/client/app/desktop/views/pages/user/user.profile.vue @@ -3,8 +3,14 @@

%i18n:@follows-you%

-

%i18n:@muted% %i18n:@unmute%

-

%i18n:@mute%

+

+ %i18n:@stalking% %i18n:@unstalk% + %i18n:@stalk% +

+

+ %i18n:@muted% %i18n:@unmute% + %i18n:@mute% +

{{ user.description }}
@@ -47,6 +53,26 @@ export default Vue.extend({ }); }, + stalk() { + (this as any).api('following/stalk', { + userId: this.user.id + }).then(() => { + this.user.isStalking = true; + }, () => { + alert('error'); + }); + }, + + unstalk() { + (this as any).api('following/unstalk', { + userId: this.user.id + }).then(() => { + this.user.isStalking = false; + }, () => { + alert('error'); + }); + }, + mute() { (this as any).api('mute/create', { userId: this.user.id diff --git a/src/models/following.ts b/src/models/following.ts index f10e349ee9..4712379a70 100644 --- a/src/models/following.ts +++ b/src/models/following.ts @@ -10,6 +10,17 @@ export type IFollowing = { createdAt: Date; followeeId: mongo.ObjectID; followerId: mongo.ObjectID; + stalk: boolean; + + // 非正規化 + _followee: { + host: string; + inbox?: string; + }, + _follower: { + host: string; + inbox?: string; + } }; /** diff --git a/src/models/note.ts b/src/models/note.ts index 3059593540..d4b16afa4c 100644 --- a/src/models/note.ts +++ b/src/models/note.ts @@ -58,6 +58,7 @@ export type INote = { }; uri: string; + // 非正規化 _reply?: { userId: mongo.ObjectID; }; @@ -66,9 +67,7 @@ export type INote = { }; _user: { host: string; - account: { - inbox?: string; - }; + inbox?: string; }; }; diff --git a/src/models/user.ts b/src/models/user.ts index bcb2a73e24..ca1ca28937 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -5,7 +5,7 @@ import db from '../db/mongodb'; import Note, { pack as packNote, deleteNote } from './note'; import Following, { deleteFollowing } from './following'; import Mute, { deleteMute } from './mute'; -import getFriends from '../server/api/common/get-friends'; +import { getFriendIds } from '../server/api/common/get-friends'; import config from '../config'; import AccessToken, { deleteAccessToken } from './access-token'; import NoteWatching, { deleteNoteWatching } from './note-watching'; @@ -375,33 +375,30 @@ export const pack = ( } if (meId && !meId.equals(_user.id)) { - // Whether the user is following - _user.isFollowing = (async () => { - const follow = await Following.findOne({ + const [following1, following2, mute] = await Promise.all([ + Following.findOne({ followerId: meId, followeeId: _user.id - }); - return follow !== null; - })(); - - // Whether the user is followed - _user.isFollowed = (async () => { - const follow2 = await Following.findOne({ + }), + Following.findOne({ followerId: _user.id, followeeId: meId - }); - return follow2 !== null; - })(); + }), + Mute.findOne({ + muterId: meId, + muteeId: _user.id + }) + ]); + + // Whether the user is following + _user.isFollowing = following1 !== null; + _user.isStalking = following1 && following1.stalk; + + // Whether the user is followed + _user.isFollowed = following2 !== null; // Whether the user is muted - _user.isMuted = (async () => { - const mute = await Mute.findOne({ - muterId: meId, - muteeId: _user.id, - deletedAt: { $exists: false } - }); - return mute !== null; - })(); + _user.isMuted = mute !== null; } if (opts.detail) { @@ -413,7 +410,7 @@ export const pack = ( } if (meId && !meId.equals(_user.id)) { - const myFollowingIds = await getFriends(meId); + const myFollowingIds = await getFriendIds(meId); // Get following you know count _user.followingYouKnowCount = Following.count({ diff --git a/src/server/api/common/get-friends.ts b/src/server/api/common/get-friends.ts index c1cc3957d8..50ba71ea96 100644 --- a/src/server/api/common/get-friends.ts +++ b/src/server/api/common/get-friends.ts @@ -1,10 +1,10 @@ import * as mongodb from 'mongodb'; import Following from '../../../models/following'; -export default async (me: mongodb.ObjectID, includeMe: boolean = true) => { +export const getFriendIds = async (me: mongodb.ObjectID, includeMe = true) => { // Fetch relation to other users who the I follows // SELECT followee - const myfollowing = await Following + const followings = await Following .find({ followerId: me }, { @@ -14,7 +14,7 @@ export default async (me: mongodb.ObjectID, includeMe: boolean = true) => { }); // ID list of other users who the I follows - const myfollowingIds = myfollowing.map(follow => follow.followeeId); + const myfollowingIds = followings.map(following => following.followeeId); if (includeMe) { myfollowingIds.push(me); @@ -22,3 +22,26 @@ export default async (me: mongodb.ObjectID, includeMe: boolean = true) => { return myfollowingIds; }; + +export const getFriends = async (me: mongodb.ObjectID, includeMe = true) => { + // Fetch relation to other users who the I follows + const followings = await Following + .find({ + followerId: me + }); + + // ID list of other users who the I follows + const myfollowings = followings.map(following => ({ + id: following.followeeId, + stalk: following.stalk + })); + + if (includeMe) { + myfollowings.push({ + id: me, + stalk: true + }); + } + + return myfollowings; +}; diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts index e0223c23e0..7cf49debe9 100644 --- a/src/server/api/endpoints.ts +++ b/src/server/api/endpoints.ts @@ -426,6 +426,24 @@ const endpoints: Endpoint[] = [ }, kind: 'following-write' }, + { + name: 'following/stalk', + withCredential: true, + limit: { + duration: ms('1hour'), + max: 100 + }, + kind: 'following-write' + }, + { + name: 'following/unstalk', + withCredential: true, + limit: { + duration: ms('1hour'), + max: 100 + }, + kind: 'following-write' + }, { name: 'notes' diff --git a/src/server/api/endpoints/following/stalk.ts b/src/server/api/endpoints/following/stalk.ts new file mode 100644 index 0000000000..fc8be4924d --- /dev/null +++ b/src/server/api/endpoints/following/stalk.ts @@ -0,0 +1,36 @@ +import $ from 'cafy'; +import Following from '../../../../models/following'; +import { isLocalUser } from '../../../../models/user'; + +/** + * Stalk a user + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + const follower = user; + + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); + + // Fetch following + const following = await Following.findOne({ + followerId: follower._id, + followeeId: userId + }); + + if (following === null) { + return rej('following not found'); + } + + // Stalk + await Following.update({ _id: following._id }, { + $set: { + stalk: true + } + }); + + // Send response + res(); + + // TODO: イベント +}); diff --git a/src/server/api/endpoints/following/unstalk.ts b/src/server/api/endpoints/following/unstalk.ts new file mode 100644 index 0000000000..d7593bcd00 --- /dev/null +++ b/src/server/api/endpoints/following/unstalk.ts @@ -0,0 +1,35 @@ +import $ from 'cafy'; +import Following from '../../../../models/following'; + +/** + * Unstalk a user + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + const follower = user; + + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); + + // Fetch following + const following = await Following.findOne({ + followerId: follower._id, + followeeId: userId + }); + + if (following === null) { + return rej('following not found'); + } + + // Stalk + await Following.update({ _id: following._id }, { + $set: { + stalk: false + } + }); + + // Send response + res(); + + // TODO: イベント +}); diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts index 3b4899682d..69a8910898 100644 --- a/src/server/api/endpoints/i/notifications.ts +++ b/src/server/api/endpoints/i/notifications.ts @@ -5,7 +5,7 @@ import $ from 'cafy'; import Notification from '../../../../models/notification'; import Mute from '../../../../models/mute'; import { pack } from '../../../../models/notification'; -import getFriends from '../../common/get-friends'; +import { getFriendIds } from '../../common/get-friends'; import read from '../../common/read-notification'; /** @@ -62,7 +62,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (following) { // ID list of the user itself and other users who the user follows - const followingIds = await getFriends(user._id); + const followingIds = await getFriendIds(user._id); query.$and.push({ notifierId: { diff --git a/src/server/api/endpoints/mute/list.ts b/src/server/api/endpoints/mute/list.ts index bd80401445..0b8262d6c5 100644 --- a/src/server/api/endpoints/mute/list.ts +++ b/src/server/api/endpoints/mute/list.ts @@ -4,7 +4,7 @@ import $ from 'cafy'; import Mute from '../../../../models/mute'; import { pack } from '../../../../models/user'; -import getFriends from '../../common/get-friends'; +import { getFriendIds } from '../../common/get-friends'; /** * Get muted users of a user @@ -34,7 +34,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { if (iknow) { // Get my friends - const myFriends = await getFriends(me._id); + const myFriends = await getFriendIds(me._id); query.muteeId = { $in: myFriends diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts index c507acbaec..2d95606b3f 100644 --- a/src/server/api/endpoints/notes/mentions.ts +++ b/src/server/api/endpoints/notes/mentions.ts @@ -3,7 +3,7 @@ */ import $ from 'cafy'; import Note from '../../../../models/note'; -import getFriends from '../../common/get-friends'; +import { getFriendIds } from '../../common/get-friends'; import { pack } from '../../../../models/note'; /** @@ -46,7 +46,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }; if (following) { - const followingIds = await getFriends(user._id); + const followingIds = await getFriendIds(user._id); query.userId = { $in: followingIds diff --git a/src/server/api/endpoints/notes/search.ts b/src/server/api/endpoints/notes/search.ts index bfa17b000e..3ff3fbbafa 100644 --- a/src/server/api/endpoints/notes/search.ts +++ b/src/server/api/endpoints/notes/search.ts @@ -6,7 +6,7 @@ const escapeRegexp = require('escape-regexp'); import Note from '../../../../models/note'; import User from '../../../../models/user'; import Mute from '../../../../models/mute'; -import getFriends from '../../common/get-friends'; +import { getFriendIds } from '../../common/get-friends'; import { pack } from '../../../../models/note'; /** @@ -156,7 +156,7 @@ async function search( } if (following != null && me != null) { - const ids = await getFriends(me._id, false); + const ids = await getFriendIds(me._id, false); push({ userId: following ? { $in: ids diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts index b5feaac817..8cd23fd369 100644 --- a/src/server/api/endpoints/notes/timeline.ts +++ b/src/server/api/endpoints/notes/timeline.ts @@ -2,11 +2,10 @@ * Module dependencies */ import $ from 'cafy'; -import rap from '@prezzemolo/rap'; import Note from '../../../../models/note'; import Mute from '../../../../models/mute'; import ChannelWatching from '../../../../models/channel-watching'; -import getFriends from '../../common/get-friends'; +import { getFriends } from '../../common/get-friends'; import { pack } from '../../../../models/note'; /** @@ -38,41 +37,66 @@ module.exports = async (params, user, app) => { throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; } - const { followingIds, watchingChannelIds, mutedUserIds } = await rap({ - // ID list of the user itself and other users who the user follows - followingIds: getFriends(user._id), + const [followings, watchingChannelIds, mutedUserIds] = await Promise.all([ + // フォローを取得 + // Fetch following + getFriends(user._id), // Watchしているチャンネルを取得 - watchingChannelIds: ChannelWatching.find({ + ChannelWatching.find({ userId: user._id, // 削除されたドキュメントは除く deletedAt: { $exists: false } }).then(watches => watches.map(w => w.channelId)), // ミュートしているユーザーを取得 - mutedUserIds: Mute.find({ + Mute.find({ muterId: user._id }).then(ms => ms.map(m => m.muteeId)) - }); + ]); //#region Construct query const sort = { _id: -1 }; + const followQuery = followings.map(f => f.stalk ? { + userId: f.id + } : { + userId: f.id, + + // ストーキングしてないならリプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める) + $or: [{ + // リプライでない + replyId: null + }, { // または + // リプライだが返信先が投稿者自身の投稿 + $expr: { + '$_reply.userId': '$userId' + } + }, { // または + // リプライだが返信先が自分(フォロワー)の投稿 + '_reply.userId': user._id + }, { // または + // 自分(フォロワー)が送信したリプライ + userId: user._id + }] + }); + const query = { $or: [{ - // フォローしている人のタイムラインへの投稿 - userId: { - $in: followingIds - }, - // 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る - $or: [{ - channelId: { - $exists: false - } + $and: [{ + // フォローしている人のタイムラインへの投稿 + $or: followQuery }, { - channelId: null + // 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る + $or: [{ + channelId: { + $exists: false + } + }, { + channelId: null + }] }] }, { // Watchしているチャンネルへの投稿 diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts index 0222313e81..5f03326be8 100644 --- a/src/server/api/endpoints/users/followers.ts +++ b/src/server/api/endpoints/users/followers.ts @@ -5,7 +5,7 @@ import $ from 'cafy'; import User from '../../../../models/user'; import Following from '../../../../models/following'; import { pack } from '../../../../models/user'; -import getFriends from '../../common/get-friends'; +import { getFriendIds } from '../../common/get-friends'; /** * Get followers of a user @@ -52,7 +52,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // ログインしていてかつ iknow フラグがあるとき if (me && iknow) { // Get my friends - const myFriends = await getFriends(me._id); + const myFriends = await getFriendIds(me._id); query.followerId = { $in: myFriends diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts index 2372f57fbe..9fb135b24d 100644 --- a/src/server/api/endpoints/users/following.ts +++ b/src/server/api/endpoints/users/following.ts @@ -5,7 +5,7 @@ import $ from 'cafy'; import User from '../../../../models/user'; import Following from '../../../../models/following'; import { pack } from '../../../../models/user'; -import getFriends from '../../common/get-friends'; +import { getFriendIds } from '../../common/get-friends'; /** * Get following users of a user @@ -52,7 +52,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // ログインしていてかつ iknow フラグがあるとき if (me && iknow) { // Get my friends - const myFriends = await getFriends(me._id); + const myFriends = await getFriendIds(me._id); query.followeeId = { $in: myFriends diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts index 2a6d36b75b..f72bb04bf1 100644 --- a/src/server/api/endpoints/users/recommendation.ts +++ b/src/server/api/endpoints/users/recommendation.ts @@ -4,7 +4,7 @@ const ms = require('ms'); import $ from 'cafy'; import User, { pack } from '../../../../models/user'; -import getFriends from '../../common/get-friends'; +import { getFriendIds } from '../../common/get-friends'; import Mute from '../../../../models/mute'; /** @@ -24,7 +24,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { 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 followingIds = await getFriendIds(me._id); // ミュートしているユーザーを取得 const mutedUserIds = (await Mute.find({ diff --git a/src/services/following/create.ts b/src/services/following/create.ts index 375b028912..3424c55dae 100644 --- a/src/services/following/create.ts +++ b/src/services/following/create.ts @@ -13,7 +13,18 @@ export default async function(follower: IUser, followee: IUser, activity?) { const following = await Following.insert({ createdAt: new Date(), followerId: follower._id, - followeeId: followee._id + followeeId: followee._id, + stalk: true, + + // 非正規化 + _follower: { + host: follower.host, + inbox: isRemoteUser(follower) ? follower.inbox : undefined + }, + _followee: { + host: followee.host, + inbox: isRemoteUser(followee) ? followee.inbox : undefined + } }); //#region Increment following count diff --git a/src/services/note/create.ts b/src/services/note/create.ts index bdef5e09fc..32db77011f 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -124,19 +124,8 @@ export default async (user: IUser, data: { publishGlobalTimelineStream(noteObj); // Fetch all followers - const followers = await Following.aggregate([{ - $lookup: { - from: 'users', - localField: 'followerId', - foreignField: '_id', - as: 'user' - } - }, { - $match: { - followeeId: note.userId - } - }], { - _id: false + const followers = await Following.find({ + followeeId: note.userId }); if (!silent) { @@ -157,12 +146,15 @@ export default async (user: IUser, data: { deliver(user, await render(), data.renote._user.inbox); } - Promise.all(followers.map(async follower => { - follower = follower.user[0]; + Promise.all(followers.map(async following => { + const follower = following._follower; if (isLocalUser(follower)) { + // この投稿が返信かつstalkフォローでないならスキップ + if (note.replyId && !following.stalk) return; + // Publish event to followers stream - stream(follower._id, 'note', noteObj); + stream(following.followerId, 'note', noteObj); } else { // フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信 if (isLocalUser(user)) {