User blocking (Following part) (#3035)

* block wip

* UndoBlock

* UnBlock

* wip

* follow

* UI

* fix
This commit is contained in:
MeiMei 2018-10-29 20:32:42 +09:00 committed by syuilo
parent bcb0588409
commit d64dc45899
17 changed files with 537 additions and 4 deletions

View file

@ -1203,6 +1203,9 @@ desktop/views/pages/user/user.profile.vue:
mute: "ミュートする" mute: "ミュートする"
muted: "ミュートしています" muted: "ミュートしています"
unmute: "ミュート解除" unmute: "ミュート解除"
block: "ブロックする"
unblock: "ブロック解除"
block-confirm: "このユーザーをブロックしますか?"
push-to-a-list: "リストに追加" push-to-a-list: "リストに追加"
list-pushed: "{user}を{list}に追加しました。" list-pushed: "{user}を{list}に追加しました。"

View file

@ -13,6 +13,10 @@
<span v-if="user.isMuted">%fa:eye% %i18n:@unmute%</span> <span v-if="user.isMuted">%fa:eye% %i18n:@unmute%</span>
<span v-if="!user.isMuted">%fa:eye-slash% %i18n:@mute%</span> <span v-if="!user.isMuted">%fa:eye-slash% %i18n:@mute%</span>
</ui-button> </ui-button>
<ui-button @click="user.isBlocking ? unblock() : block()" v-if="$store.state.i.id != user.id">
<span v-if="user.isBlocking">%fa:user% %i18n:@unblock%</span>
<span v-if="!user.isBlocking">%fa:user-slash% %i18n:@block%</span>
</ui-button>
<ui-button @click="list">%fa:list% %i18n:@push-to-a-list%</ui-button> <ui-button @click="list">%fa:list% %i18n:@push-to-a-list%</ui-button>
</div> </div>
</div> </div>
@ -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() { list() {
const w = (this as any).os.new(MkUserListsWindow); const w = (this as any).os.new(MkUserListsWindow);
w.$once('choosen', async list => { w.$once('choosen', async list => {

41
src/models/blocking.ts Normal file
View file

@ -0,0 +1,41 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid';
const Blocking = db.get<IBlocking>('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
});
}

View file

@ -6,6 +6,7 @@ import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid'; import isObjectId from '../misc/is-objectid';
import Note, { packMany as packNoteMany, deleteNote } from './note'; import Note, { packMany as packNoteMany, deleteNote } from './note';
import Following, { deleteFollowing } from './following'; import Following, { deleteFollowing } from './following';
import Blocking, { deleteBlocking } from './blocking';
import Mute, { deleteMute } from './mute'; import Mute, { deleteMute } from './mute';
import { getFriendIds } from '../server/api/common/get-friends'; import { getFriendIds } from '../server/api/common/get-friends';
import config from '../config'; import config from '../config';
@ -275,6 +276,16 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) {
await FollowRequest.find({ followeeId: u._id }) await FollowRequest.find({ followeeId: u._id })
).map(x => deleteFollowRequest(x))); ).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をすべて削除 // このユーザーのSwSubscriptionをすべて削除
await Promise.all(( await Promise.all((
await SwSubscription.find({ userId: u._id }) await SwSubscription.find({ userId: u._id })
@ -427,7 +438,7 @@ export const pack = (
} }
if (meId && !meId.equals(_user.id)) { 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({ Following.findOne({
followerId: meId, followerId: meId,
followeeId: _user.id followeeId: _user.id
@ -444,6 +455,14 @@ export const pack = (
followerId: _user.id, followerId: _user.id,
followeeId: meId followeeId: meId
}), }),
Blocking.findOne({
blockerId: meId,
blockeeId: _user.id
}),
Blocking.findOne({
blockerId: _user.id,
blockeeId: meId
}),
Mute.findOne({ Mute.findOne({
muterId: meId, muterId: meId,
muteeId: _user.id muteeId: _user.id
@ -460,6 +479,12 @@ export const pack = (
// Whether the user is followed // Whether the user is followed
_user.isFollowed = following2 !== null; _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 // Whether the user is muted
_user.isMuted = mute !== null; _user.isMuted = mute !== null;
} }

View file

@ -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<void> => {
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);
};

View file

@ -10,6 +10,7 @@ import accept from './accept';
import reject from './reject'; import reject from './reject';
import add from './add'; import add from './add';
import remove from './remove'; import remove from './remove';
import block from './block';
const self = async (actor: IRemoteUser, activity: Object): Promise<void> => { const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
switch (activity.type) { switch (activity.type) {
@ -53,6 +54,10 @@ const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
await undo(actor, activity); await undo(actor, activity);
break; break;
case 'Block':
await block(actor, activity);
break;
case 'Collection': case 'Collection':
case 'OrderedCollection': case 'OrderedCollection':
// TODO // TODO

View file

@ -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<void> => {
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);
};

View file

@ -1,8 +1,9 @@
import * as debug from 'debug'; import * as debug from 'debug';
import { IRemoteUser } from '../../../../models/user'; import { IRemoteUser } from '../../../../models/user';
import { IUndo, IFollow } from '../../type'; import { IUndo, IFollow, IBlock } from '../../type';
import unfollow from './follow'; import unfollow from './follow';
import unblock from './block';
import Resolver from '../../resolver'; import Resolver from '../../resolver';
const log = debug('misskey:activitypub'); const log = debug('misskey:activitypub');
@ -31,6 +32,9 @@ export default async (actor: IRemoteUser, activity: IUndo): Promise<void> => {
case 'Follow': case 'Follow':
unfollow(actor, object as IFollow); unfollow(actor, object as IFollow);
break; break;
case 'Block':
unblock(actor, object as IBlock);
break;
} }
return null; return null;

View file

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

View file

@ -108,6 +108,10 @@ export interface IAnnounce extends IActivity {
type: 'Announce'; type: 'Announce';
} }
export interface IBlock extends IActivity {
type: 'Block';
}
export type Object = export type Object =
ICollection | ICollection |
IOrderedCollection | IOrderedCollection |
@ -120,4 +124,5 @@ export type Object =
IAdd | IAdd |
IRemove | IRemove |
ILike | ILike |
IAnnounce; IAnnounce |
IBlock;

View file

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

View file

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

View file

@ -68,7 +68,11 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
} }
// Create following // Create following
try {
await create(follower, followee); await create(follower, followee);
} catch (e) {
return rej(e && e.message ? e.message : e);
}
// Send response // Send response
res(await pack(followee._id, user)); res(await pack(followee._id, user));

View file

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

View file

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

View file

@ -1,15 +1,45 @@
import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user';
import Following from '../../models/following'; import Following from '../../models/following';
import Blocking from '../../models/blocking';
import { publishMainStream } from '../../stream'; import { publishMainStream } from '../../stream';
import notify from '../../notify'; import notify from '../../notify';
import pack from '../../remote/activitypub/renderer'; import pack from '../../remote/activitypub/renderer';
import renderFollow from '../../remote/activitypub/renderer/follow'; import renderFollow from '../../remote/activitypub/renderer/follow';
import renderAccept from '../../remote/activitypub/renderer/accept'; import renderAccept from '../../remote/activitypub/renderer/accept';
import renderReject from '../../remote/activitypub/renderer/reject';
import { deliver } from '../../queue'; import { deliver } from '../../queue';
import createFollowRequest from './requests/create'; import createFollowRequest from './requests/create';
import perUserFollowingChart from '../../chart/per-user-following'; import perUserFollowingChart from '../../chart/per-user-following';
export default async function(follower: IUser, followee: IUser, requestId?: string) { 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 // フォロー対象が鍵アカウントである or
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである // フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである

View file

@ -5,8 +5,24 @@ import pack from '../../../remote/activitypub/renderer';
import renderFollow from '../../../remote/activitypub/renderer/follow'; import renderFollow from '../../../remote/activitypub/renderer/follow';
import { deliver } from '../../../queue'; import { deliver } from '../../../queue';
import FollowRequest from '../../../models/follow-request'; import FollowRequest from '../../../models/follow-request';
import Blocking from '../../../models/blocking';
export default async function(follower: IUser, followee: IUser, requestId?: string) { 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({ await FollowRequest.insert({
createdAt: new Date(), createdAt: new Date(),
followerId: follower._id, followerId: follower._id,