From d64dc458999afdc0bfd5f662a583bd1a0f6eebb3 Mon Sep 17 00:00:00 2001 From: MeiMei <30769358+mei23@users.noreply.github.com> Date: Mon, 29 Oct 2018 20:32:42 +0900 Subject: [PATCH] User blocking (Following part) (#3035) * block wip * UndoBlock * UnBlock * wip * follow * UI * fix --- locales/ja-JP.yml | 3 + .../desktop/views/pages/user/user.profile.vue | 25 ++++ src/models/blocking.ts | 41 ++++++ src/models/user.ts | 27 +++- src/remote/activitypub/kernel/block/index.ts | 34 +++++ src/remote/activitypub/kernel/index.ts | 5 + src/remote/activitypub/kernel/undo/block.ts | 34 +++++ src/remote/activitypub/kernel/undo/index.ts | 6 +- src/remote/activitypub/renderer/block.ts | 8 ++ src/remote/activitypub/type.ts | 7 +- src/server/api/endpoints/blocking/create.ts | 75 +++++++++++ src/server/api/endpoints/blocking/delete.ts | 75 +++++++++++ src/server/api/endpoints/following/create.ts | 6 +- src/services/blocking/create.ts | 121 ++++++++++++++++++ src/services/blocking/delete.ts | 28 ++++ src/services/following/create.ts | 30 +++++ src/services/following/requests/create.ts | 16 +++ 17 files changed, 537 insertions(+), 4 deletions(-) create mode 100644 src/models/blocking.ts create mode 100644 src/remote/activitypub/kernel/block/index.ts create mode 100644 src/remote/activitypub/kernel/undo/block.ts create mode 100644 src/remote/activitypub/renderer/block.ts create mode 100644 src/server/api/endpoints/blocking/create.ts create mode 100644 src/server/api/endpoints/blocking/delete.ts create mode 100644 src/services/blocking/create.ts create mode 100644 src/services/blocking/delete.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 4cb4d4791..267c49e31 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1203,6 +1203,9 @@ desktop/views/pages/user/user.profile.vue: mute: "ミュートする" muted: "ミュートしています" unmute: "ミュート解除" + block: "ブロックする" + unblock: "ブロック解除" + block-confirm: "このユーザーをブロックしますか?" push-to-a-list: "リストに追加" list-pushed: "{user}を{list}に追加しました。" 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 fe864f0d7..a075995e6 100644 --- a/src/client/app/desktop/views/pages/user/user.profile.vue +++ b/src/client/app/desktop/views/pages/user/user.profile.vue @@ -13,6 +13,10 @@ %fa:eye% %i18n:@unmute% %fa:eye-slash% %i18n:@mute% + + %fa:user% %i18n:@unblock% + %fa:user-slash% %i18n:@block% + %fa:list% %i18n:@push-to-a-list% @@ -66,6 +70,27 @@ export default Vue.extend({ }); }, + block() { + if (!window.confirm('%i18n:@block-confirm%')) return; + (this as any).api('blocking/create', { + userId: this.user.id + }).then(() => { + this.user.isBlocking = true; + }, () => { + alert('error'); + }); + }, + + unblock() { + (this as any).api('blocking/delete', { + userId: this.user.id + }).then(() => { + this.user.isBlocking = false; + }, () => { + alert('error'); + }); + }, + list() { const w = (this as any).os.new(MkUserListsWindow); w.$once('choosen', async list => { diff --git a/src/models/blocking.ts b/src/models/blocking.ts new file mode 100644 index 000000000..9a6e4ce42 --- /dev/null +++ b/src/models/blocking.ts @@ -0,0 +1,41 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; +import isObjectId from '../misc/is-objectid'; + +const Blocking = db.get('blocking'); +Blocking.createIndex(['blockerId', 'blockeeId'], { unique: true }); +export default Blocking; + +export type IBlocking = { + _id: mongo.ObjectID; + createdAt: Date; + blockeeId: mongo.ObjectID; + blockerId: mongo.ObjectID; +}; + +/** + * Blockingを物理削除します + */ +export async function deleteBlocking(blocking: string | mongo.ObjectID | IBlocking) { + let f: IBlocking; + + // Populate + if (isObjectId(blocking)) { + f = await Blocking.findOne({ + _id: blocking + }); + } else if (typeof blocking === 'string') { + f = await Blocking.findOne({ + _id: new mongo.ObjectID(blocking) + }); + } else { + f = blocking as IBlocking; + } + + if (f == null) return; + + // このBlockingを削除 + await Blocking.remove({ + _id: f._id + }); +} diff --git a/src/models/user.ts b/src/models/user.ts index 25c4a9eb0..e13802595 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -6,6 +6,7 @@ import db from '../db/mongodb'; import isObjectId from '../misc/is-objectid'; import Note, { packMany as packNoteMany, deleteNote } from './note'; import Following, { deleteFollowing } from './following'; +import Blocking, { deleteBlocking } from './blocking'; import Mute, { deleteMute } from './mute'; import { getFriendIds } from '../server/api/common/get-friends'; import config from '../config'; @@ -275,6 +276,16 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) { await FollowRequest.find({ followeeId: u._id }) ).map(x => deleteFollowRequest(x))); + // このユーザーのBlockingをすべて削除 + await Promise.all(( + await Blocking.find({ blockerId: u._id }) + ).map(x => deleteBlocking(x))); + + // このユーザーへのBlockingをすべて削除 + await Promise.all(( + await Blocking.find({ blockeeId: u._id }) + ).map(x => deleteBlocking(x))); + // このユーザーのSwSubscriptionをすべて削除 await Promise.all(( await SwSubscription.find({ userId: u._id }) @@ -427,7 +438,7 @@ export const pack = ( } if (meId && !meId.equals(_user.id)) { - const [following1, following2, followReq1, followReq2, mute] = await Promise.all([ + const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([ Following.findOne({ followerId: meId, followeeId: _user.id @@ -444,6 +455,14 @@ export const pack = ( followerId: _user.id, followeeId: meId }), + Blocking.findOne({ + blockerId: meId, + blockeeId: _user.id + }), + Blocking.findOne({ + blockerId: _user.id, + blockeeId: meId + }), Mute.findOne({ muterId: meId, muteeId: _user.id @@ -460,6 +479,12 @@ export const pack = ( // Whether the user is followed _user.isFollowed = following2 !== null; + // Whether the user is blocking + _user.isBlocking = toBlocking !== null; + + // Whether the user is blocked + _user.isBlocked = fromBlocked !== null; + // Whether the user is muted _user.isMuted = mute !== null; } diff --git a/src/remote/activitypub/kernel/block/index.ts b/src/remote/activitypub/kernel/block/index.ts new file mode 100644 index 000000000..dec591acc --- /dev/null +++ b/src/remote/activitypub/kernel/block/index.ts @@ -0,0 +1,34 @@ +import * as mongo from 'mongodb'; +import User, { IRemoteUser } from '../../../../models/user'; +import config from '../../../../config'; +import * as debug from 'debug'; +import { IBlock } from '../../type'; +import block from '../../../../services/blocking/create'; + +const log = debug('misskey:activitypub'); + +export default async (actor: IRemoteUser, activity: IBlock): Promise => { + const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + + const uri = activity.id || activity; + + log(`Block: ${uri}`); + + if (!id.startsWith(config.url + '/')) { + return null; + } + + const blockee = await User.findOne({ + _id: new mongo.ObjectID(id.split('/').pop()) + }); + + if (blockee === null) { + throw new Error('blockee not found'); + } + + if (blockee.host != null) { + throw new Error('ブロックしようとしているユーザーはローカルユーザーではありません'); + } + + block(actor, blockee); +}; diff --git a/src/remote/activitypub/kernel/index.ts b/src/remote/activitypub/kernel/index.ts index 52b0efc73..61bb89f5e 100644 --- a/src/remote/activitypub/kernel/index.ts +++ b/src/remote/activitypub/kernel/index.ts @@ -10,6 +10,7 @@ import accept from './accept'; import reject from './reject'; import add from './add'; import remove from './remove'; +import block from './block'; const self = async (actor: IRemoteUser, activity: Object): Promise => { switch (activity.type) { @@ -53,6 +54,10 @@ const self = async (actor: IRemoteUser, activity: Object): Promise => { await undo(actor, activity); break; + case 'Block': + await block(actor, activity); + break; + case 'Collection': case 'OrderedCollection': // TODO diff --git a/src/remote/activitypub/kernel/undo/block.ts b/src/remote/activitypub/kernel/undo/block.ts new file mode 100644 index 000000000..b735f114d --- /dev/null +++ b/src/remote/activitypub/kernel/undo/block.ts @@ -0,0 +1,34 @@ +import * as mongo from 'mongodb'; +import User, { IRemoteUser } from '../../../../models/user'; +import config from '../../../../config'; +import * as debug from 'debug'; +import { IBlock } from '../../type'; +import unblock from '../../../../services/blocking/delete'; + +const log = debug('misskey:activitypub'); + +export default async (actor: IRemoteUser, activity: IBlock): Promise => { + const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + + const uri = activity.id || activity; + + log(`UnBlock: ${uri}`); + + if (!id.startsWith(config.url + '/')) { + return null; + } + + const blockee = await User.findOne({ + _id: new mongo.ObjectID(id.split('/').pop()) + }); + + if (blockee === null) { + throw new Error('blockee not found'); + } + + if (blockee.host != null) { + throw new Error('ブロック解除しようとしているユーザーはローカルユーザーではありません'); + } + + unblock(actor, blockee); +}; diff --git a/src/remote/activitypub/kernel/undo/index.ts b/src/remote/activitypub/kernel/undo/index.ts index 5d9535403..ba56dd632 100644 --- a/src/remote/activitypub/kernel/undo/index.ts +++ b/src/remote/activitypub/kernel/undo/index.ts @@ -1,8 +1,9 @@ import * as debug from 'debug'; import { IRemoteUser } from '../../../../models/user'; -import { IUndo, IFollow } from '../../type'; +import { IUndo, IFollow, IBlock } from '../../type'; import unfollow from './follow'; +import unblock from './block'; import Resolver from '../../resolver'; const log = debug('misskey:activitypub'); @@ -31,6 +32,9 @@ export default async (actor: IRemoteUser, activity: IUndo): Promise => { case 'Follow': unfollow(actor, object as IFollow); break; + case 'Block': + unblock(actor, object as IBlock); + break; } return null; diff --git a/src/remote/activitypub/renderer/block.ts b/src/remote/activitypub/renderer/block.ts new file mode 100644 index 000000000..316fc13c0 --- /dev/null +++ b/src/remote/activitypub/renderer/block.ts @@ -0,0 +1,8 @@ +import config from '../../../config'; +import { ILocalUser, IRemoteUser } from "../../../models/user"; + +export default (blocker?: ILocalUser, blockee?: IRemoteUser) => ({ + type: 'Block', + actor: `${config.url}/users/${blocker._id}`, + object: blockee.uri +}); diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index 5c06ee4ff..234403501 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -108,6 +108,10 @@ export interface IAnnounce extends IActivity { type: 'Announce'; } +export interface IBlock extends IActivity { + type: 'Block'; +} + export type Object = ICollection | IOrderedCollection | @@ -120,4 +124,5 @@ export type Object = IAdd | IRemove | ILike | - IAnnounce; + IAnnounce | + IBlock; diff --git a/src/server/api/endpoints/blocking/create.ts b/src/server/api/endpoints/blocking/create.ts new file mode 100644 index 000000000..c23573119 --- /dev/null +++ b/src/server/api/endpoints/blocking/create.ts @@ -0,0 +1,75 @@ +import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; +const ms = require('ms'); +import User, { pack, ILocalUser } from '../../../../models/user'; +import Blocking from '../../../../models/blocking'; +import create from '../../../../services/blocking/create'; +import getParams from '../../get-params'; + +export const meta = { + stability: 'stable', + + desc: { + 'ja-JP': '指定したユーザーをブロックします。', + 'en-US': 'Block a user.' + }, + + limit: { + duration: ms('1hour'), + max: 100 + }, + + requireCredential: true, + + kind: 'following-write', + + params: { + userId: $.type(ID).note({ + desc: { + 'ja-JP': '対象のユーザーのID', + 'en-US': 'Target user ID' + } + }) + } +}; + +export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) return rej(psErr); + + const blocker = user; + + // 自分自身 + if (user._id.equals(ps.userId)) { + return rej('blockee is yourself'); + } + + // Get blockee + const blockee = await User.findOne({ + _id: ps.userId + }, { + fields: { + data: false, + profile: false + } + }); + + if (blockee === null) { + return rej('user not found'); + } + + // Check if already blocking + const exist = await Blocking.findOne({ + blockerId: blocker._id, + blockeeId: blockee._id + }); + + if (exist !== null) { + return rej('already blocking'); + } + + // Create blocking + await create(blocker, blockee); + + // Send response + res(await pack(blockee._id, user)); +}); diff --git a/src/server/api/endpoints/blocking/delete.ts b/src/server/api/endpoints/blocking/delete.ts new file mode 100644 index 000000000..dd0cf6b51 --- /dev/null +++ b/src/server/api/endpoints/blocking/delete.ts @@ -0,0 +1,75 @@ +import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; +const ms = require('ms'); +import User, { pack, ILocalUser } from '../../../../models/user'; +import Blocking from '../../../../models/blocking'; +import deleteBlocking from '../../../../services/blocking/delete'; +import getParams from '../../get-params'; + +export const meta = { + stability: 'stable', + + desc: { + 'ja-JP': '指定したユーザーのブロックを解除します。', + 'en-US': 'Unblock a user.' + }, + + limit: { + duration: ms('1hour'), + max: 100 + }, + + requireCredential: true, + + kind: 'following-write', + + params: { + userId: $.type(ID).note({ + desc: { + 'ja-JP': '対象のユーザーのID', + 'en-US': 'Target user ID' + } + }) + } +}; + +export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) return rej(psErr); + + const blocker = user; + + // Check if the blockee is yourself + if (user._id.equals(ps.userId)) { + return rej('blockee is yourself'); + } + + // Get blockee + const blockee = await User.findOne({ + _id: ps.userId + }, { + fields: { + data: false, + 'profile': false + } + }); + + if (blockee === null) { + return rej('user not found'); + } + + // Check not blocking + const exist = await Blocking.findOne({ + blockerId: blocker._id, + blockeeId: blockee._id + }); + + if (exist === null) { + return rej('already not blocking'); + } + + // Delete blocking + await deleteBlocking(blocker, blockee); + + // Send response + res(await pack(blockee._id, user)); +}); diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts index 372bad022..028a2aa82 100644 --- a/src/server/api/endpoints/following/create.ts +++ b/src/server/api/endpoints/following/create.ts @@ -68,7 +68,11 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = } // Create following - await create(follower, followee); + try { + await create(follower, followee); + } catch (e) { + return rej(e && e.message ? e.message : e); + } // Send response res(await pack(followee._id, user)); diff --git a/src/services/blocking/create.ts b/src/services/blocking/create.ts new file mode 100644 index 000000000..11b2954af --- /dev/null +++ b/src/services/blocking/create.ts @@ -0,0 +1,121 @@ +import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; +import Following from '../../models/following'; +import FollowRequest from '../../models/follow-request'; +import { publishMainStream } from '../../stream'; +import pack from '../../remote/activitypub/renderer'; +import renderFollow from '../../remote/activitypub/renderer/follow'; +import renderUndo from '../../remote/activitypub/renderer/undo'; +import renderBlock from '../../remote/activitypub/renderer/block'; +import { deliver } from '../../queue'; +import renderReject from '../../remote/activitypub/renderer/reject'; +import perUserFollowingChart from '../../chart/per-user-following'; +import Blocking from '../../models/blocking'; + +export default async function(blocker: IUser, blockee: IUser) { + + await Promise.all([ + cancelRequest(blocker, blockee), + cancelRequest(blockee, blocker), + unFollow(blocker, blockee), + unFollow(blockee, blocker) + ]); + + await Blocking.insert({ + createdAt: new Date(), + blockerId: blocker._id, + blockeeId: blockee._id, + }); + + if (isLocalUser(blocker) && isRemoteUser(blockee)) { + const content = pack(renderBlock(blocker, blockee)); + deliver(blocker, content, blockee.inbox); + } +} + +async function cancelRequest(follower: IUser, followee: IUser) { + const request = await FollowRequest.findOne({ + followeeId: followee._id, + followerId: follower._id + }); + + if (request == null) { + return; + } + + await FollowRequest.remove({ + followeeId: followee._id, + followerId: follower._id + }); + + await User.update({ _id: followee._id }, { + $inc: { + pendingReceivedFollowRequestsCount: -1 + } + }); + + if (isLocalUser(followee)) { + packUser(followee, followee, { + detail: true + }).then(packed => publishMainStream(followee._id, 'meUpdated', packed)); + } + + if (isLocalUser(follower)) { + packUser(followee, follower).then(packed => publishMainStream(follower._id, 'unfollow', packed)); + } + + // リモートにフォローリクエストをしていたらUndoFollow送信 + if (isLocalUser(follower) && isRemoteUser(followee)) { + const content = pack(renderUndo(renderFollow(follower, followee), follower)); + deliver(follower, content, followee.inbox); + } + + // リモートからフォローリクエストを受けていたらReject送信 + if (isRemoteUser(follower) && isLocalUser(followee)) { + const content = pack(renderReject(renderFollow(follower, followee, request.requestId), followee)); + deliver(followee, content, follower.inbox); + } +} + +async function unFollow(follower: IUser, followee: IUser) { + const following = await Following.findOne({ + followerId: follower._id, + followeeId: followee._id + }); + + if (following == null) { + return; + } + + Following.remove({ + _id: following._id + }); + + //#region Decrement following count + User.update({ _id: follower._id }, { + $inc: { + followingCount: -1 + } + }); + //#endregion + + //#region Decrement followers count + User.update({ _id: followee._id }, { + $inc: { + followersCount: -1 + } + }); + //#endregion + + perUserFollowingChart.update(follower, followee, false); + + // Publish unfollow event + if (isLocalUser(follower)) { + packUser(followee, follower).then(packed => publishMainStream(follower._id, 'unfollow', packed)); + } + + // リモートにフォローをしていたらUndoFollow送信 + if (isLocalUser(follower) && isRemoteUser(followee)) { + const content = pack(renderUndo(renderFollow(follower, followee), follower)); + deliver(follower, content, followee.inbox); + } +} diff --git a/src/services/blocking/delete.ts b/src/services/blocking/delete.ts new file mode 100644 index 000000000..bc331d491 --- /dev/null +++ b/src/services/blocking/delete.ts @@ -0,0 +1,28 @@ +import { isLocalUser, isRemoteUser, IUser } from '../../models/user'; +import Blocking from '../../models/blocking'; +import pack from '../../remote/activitypub/renderer'; +import renderBlock from '../../remote/activitypub/renderer/block'; +import renderUndo from '../../remote/activitypub/renderer/undo'; +import { deliver } from '../../queue'; + +export default async function(blocker: IUser, blockee: IUser) { + const blocking = await Blocking.findOne({ + blockerId: blocker._id, + blockeeId: blockee._id + }); + + if (blocking == null) { + console.warn('ブロック解除がリクエストされましたがブロックしていませんでした'); + return; + } + + Blocking.remove({ + _id: blocking._id + }); + + // deliver if remote bloking + if (isLocalUser(blocker) && isRemoteUser(blockee)) { + const content = pack(renderUndo(renderBlock(blocker, blockee), blocker)); + deliver(blocker, content, blockee.inbox); + } +} diff --git a/src/services/following/create.ts b/src/services/following/create.ts index 87d13c444..38367399e 100644 --- a/src/services/following/create.ts +++ b/src/services/following/create.ts @@ -1,15 +1,45 @@ import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; import Following from '../../models/following'; +import Blocking from '../../models/blocking'; import { publishMainStream } from '../../stream'; import notify from '../../notify'; import pack from '../../remote/activitypub/renderer'; import renderFollow from '../../remote/activitypub/renderer/follow'; import renderAccept from '../../remote/activitypub/renderer/accept'; +import renderReject from '../../remote/activitypub/renderer/reject'; import { deliver } from '../../queue'; import createFollowRequest from './requests/create'; import perUserFollowingChart from '../../chart/per-user-following'; export default async function(follower: IUser, followee: IUser, requestId?: string) { + // check blocking + const [ blocking, blocked ] = await Promise.all([ + Blocking.findOne({ + blockerId: follower._id, + blockeeId: followee._id, + }), + Blocking.findOne({ + blockerId: followee._id, + blockeeId: follower._id, + }) + ]); + + if (isRemoteUser(follower) && isLocalUser(followee) && blocked) { + // リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。 + const content = pack(renderReject(renderFollow(follower, followee, requestId), followee)); + deliver(followee , content, follower.inbox); + return; + } else if (isRemoteUser(follower) && isLocalUser(followee) && blocking) { + // リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。 + await Blocking.remove({ + _id: blocking._id + }); + } else { + // それ以外は単純に例外 + if (blocking != null) throw new Error('blocking'); + if (blocked != null) throw new Error('blocked'); + } + // フォロー対象が鍵アカウントである or // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or // フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである diff --git a/src/services/following/requests/create.ts b/src/services/following/requests/create.ts index d28c93929..a87e472ad 100644 --- a/src/services/following/requests/create.ts +++ b/src/services/following/requests/create.ts @@ -5,8 +5,24 @@ import pack from '../../../remote/activitypub/renderer'; import renderFollow from '../../../remote/activitypub/renderer/follow'; import { deliver } from '../../../queue'; import FollowRequest from '../../../models/follow-request'; +import Blocking from '../../../models/blocking'; export default async function(follower: IUser, followee: IUser, requestId?: string) { + // check blocking + const [ blocking, blocked ] = await Promise.all([ + Blocking.findOne({ + blockerId: follower._id, + blockeeId: followee._id, + }), + Blocking.findOne({ + blockerId: followee._id, + blockeeId: follower._id, + }) + ]); + + if (blocking != null) throw new Error('blocking'); + if (blocked != null) throw new Error('blocked'); + await FollowRequest.insert({ createdAt: new Date(), followerId: follower._id,