ストーキング実装

Closes #1511
This commit is contained in:
syuilo 2018-04-19 12:43:25 +09:00
parent 1670891e7d
commit 9b05c40c2b
22 changed files with 310 additions and 80 deletions

View File

@ -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"

View File

@ -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"

View File

@ -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: "ミュート解除"

49
migration/2018-04-19.js Normal file
View File

@ -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)

View File

@ -3,8 +3,14 @@
<div class="friend-form" v-if="os.isSignedIn && os.i.id != user.id">
<mk-follow-button :user="user" size="big"/>
<p class="followed" v-if="user.isFollowed">%i18n:@follows-you%</p>
<p v-if="user.isMuted">%i18n:@muted% <a @click="unmute">%i18n:@unmute%</a></p>
<p v-if="!user.isMuted"><a @click="mute">%i18n:@mute%</a></p>
<p class="stalk">
<span v-if="user.isStalking">%i18n:@stalking% <a @click="unstalk">%i18n:@unstalk%</a></span>
<span v-if="!user.isStalking"><a @click="stalk">%i18n:@stalk%</a></span>
</p>
<p class="mute">
<span v-if="user.isMuted">%i18n:@muted% <a @click="unmute">%i18n:@unmute%</a></span>
<span v-if="!user.isMuted"><a @click="mute">%i18n:@mute%</a></span>
</p>
</div>
<div class="description" v-if="user.description">{{ user.description }}</div>
<div class="birthday" v-if="user.host === null && user.profile.birthday">
@ -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

View File

@ -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;
}
};
/**

View File

@ -58,6 +58,7 @@ export type INote = {
};
uri: string;
// 非正規化
_reply?: {
userId: mongo.ObjectID;
};
@ -66,11 +67,9 @@ export type INote = {
};
_user: {
host: string;
account: {
inbox?: string;
};
};
};
/**
* Noteを物理削除します

View File

@ -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({

View File

@ -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;
};

View File

@ -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'

View File

@ -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: イベント
});

View File

@ -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: イベント
});

View File

@ -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: {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,34 +37,58 @@ 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: [{
$and: [{
// フォローしている人のタイムラインへの投稿
userId: {
$in: followingIds
},
$or: followQuery
}, {
// 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る
$or: [{
channelId: {
@ -74,6 +97,7 @@ module.exports = async (params, user, app) => {
}, {
channelId: null
}]
}]
}, {
// Watchしているチャンネルへの投稿
channelId: {

View File

@ -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

View File

@ -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

View File

@ -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({

View File

@ -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

View File

@ -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: {
const followers = await Following.find({
followeeId: note.userId
}
}], {
_id: false
});
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)) {