enhance(server): Improve user block (#7640)
* enhance(server): Improve user block
* Update CHANGELOG.md
* ユーザーリスト対応
* 相手から見れなくなるように
* Update 1629004542760-chart-reindex.ts
2365761ba5 (commitcomment-54919821)
* update test
* add test
* add todos
* Update 1629004542760-chart-reindex.ts
			
			
This commit is contained in:
		
							parent
							
								
									7ebdd4739a
								
							
						
					
					
						commit
						7015df37e3
					
				
					 42 changed files with 394 additions and 30 deletions
				
			
		|  | @ -14,6 +14,8 @@ | |||
|   - 有効にするには、サーバー管理者がDeepLの無料アカウントを登録し、取得した認証キーを「インスタンス設定 > その他 > DeepL Auth Key」に設定する必要があります。 | ||||
| - Misskey更新時にダイアログを表示するように | ||||
| - ジョブキューウィジェットに警報音を鳴らす設定を追加 | ||||
| - ブロックの挙動を改修 | ||||
| 	- ブロックされたユーザーがブロックしたユーザーに対してアクション出来ないようになりました。詳細はドキュメントをご確認ください。 | ||||
| - UIデザインの調整 | ||||
| - データベースのインデックスを最適化 | ||||
| - Proxy使用時にKeep-Aliveをサポート | ||||
|  |  | |||
							
								
								
									
										43
									
								
								src/docs/ja-JP/features/mute-and-block.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/docs/ja-JP/features/mute-and-block.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | |||
| # ミュートとブロック | ||||
| 好みではないユーザーがいる場合は、ミュートを行うことでそのユーザーが自分から見えないようにすることができます。 | ||||
| また、より強力な措置として、ブロックを行うことでそのユーザーから自分のコンテンツが見えないようになるほか、自分に対して関わることができないようにすることができます。 | ||||
| ミュートされていることは相手は分かりませんが、ブロックされていることは相手に分かります。どちらを選ぶかはご自身の判断で行ってください。 | ||||
| 
 | ||||
| <div class="info">ℹ️ ミュートとブロックは併用できます。</div> | ||||
| 
 | ||||
| <div class="warn">⚠️ 利用規約に違反するような、迷惑なユーザーがいる場合は運営者に報告することも検討してください。</div> | ||||
| 
 | ||||
| 設定>ミュートとブロック から、自分がミュートまたはブロックしているユーザー一覧を確認することができます。 | ||||
| 
 | ||||
| ## ミュート | ||||
| ユーザーをミュートすると、そのユーザーに関する次のコンテンツがMisskeyに表示されなくなります: | ||||
| 
 | ||||
| - タイムラインや投稿の検索結果内の、そのユーザーの投稿(およびそれらの投稿に対する返信やRenote) | ||||
| - そのユーザーからの通知 | ||||
| - メッセージ履歴一覧内の、そのユーザーとのメッセージ履歴 | ||||
| - など | ||||
| 
 | ||||
| ユーザーをミュートするには、対象のユーザーのユーザーページのメニューを開き、「ミュート」ボタンを押します。 | ||||
| 
 | ||||
| <div class="info">ℹ️ ミュートを行ったことは相手に通知されず、ミュートされていることを知ることもできません。</div> | ||||
| 
 | ||||
| ## ブロック | ||||
| ユーザーをブロックすると、そのユーザーからあなたのコンテンツが見えないようになり、またあなたに対して以下のようなアクションをすることができなくなります。 | ||||
| 
 | ||||
| - フォローする | ||||
| - ユーザーリストに追加する | ||||
| - 返信する、Renoteする | ||||
| - リアクションする、アンケートに投票する | ||||
| - メッセージを送信する | ||||
| - など | ||||
| 
 | ||||
| また、 | ||||
| 
 | ||||
| - ブロックする際に既にそのユーザーからフォローされていた場合はフォローが解除されます。 | ||||
| - ブロックする際に既にそのユーザーがあなたをユーザーリストに入れていた場合はそのリストからあなたが削除されます。 | ||||
| 
 | ||||
| ユーザーをブロックするには、対象のユーザーのユーザーページのメニューを開き、「ブロック」ボタンを押します。 | ||||
| 
 | ||||
| <div class="warn">⚠️ ブロックを行ったこと自体は相手に通知されませんが、フォローを行ったりなどの上記のアクションが行えなくなるので間接的にブロックされていることは分かります。</div> | ||||
| 
 | ||||
| <div class="warn">⚠️ 相手から自分のコンテンツが見えなくなりますが、相手がアカウントを切り替えたりログアウト状態になれば見ることができます。あくまで簡易的、補助的なものとしてお考えください。</div> | ||||
|  | @ -1,13 +0,0 @@ | |||
| # ミュート | ||||
| 
 | ||||
| ユーザーをミュートすると、そのユーザーに関する次のコンテンツがMisskeyに表示されなくなります: | ||||
| 
 | ||||
| * タイムラインや投稿の検索結果内の、そのユーザーの投稿(およびそれらの投稿に対する返信やRenote) | ||||
| * そのユーザーからの通知 | ||||
| * メッセージ履歴一覧内の、そのユーザーとのメッセージ履歴 | ||||
| 
 | ||||
| ユーザーをミュートするには、対象のユーザーのユーザーページに表示されている「ミュート」ボタンを押します。 | ||||
| 
 | ||||
| ミュートを行ったことは相手に通知されず、ミュートされていることを知ることもできません。 | ||||
| 
 | ||||
| 設定>ミュート から、自分がミュートしているユーザー一覧を確認することができます。 | ||||
							
								
								
									
										15
									
								
								src/misc/is-blocker-user-related.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/misc/is-blocker-user-related.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| export function isBlockerUserRelated(note: any, blockerUserIds: Set<string>): boolean { | ||||
| 	if (blockerUserIds.has(note.userId)) { | ||||
| 		return true; | ||||
| 	} | ||||
| 
 | ||||
| 	if (note.reply != null && blockerUserIds.has(note.reply.userId)) { | ||||
| 		return true; | ||||
| 	} | ||||
| 
 | ||||
| 	if (note.renote != null && blockerUserIds.has(note.renote.userId)) { | ||||
| 		return true; | ||||
| 	} | ||||
| 
 | ||||
| 	return false; | ||||
| } | ||||
|  | @ -1,6 +1,29 @@ | |||
| import { User } from '../../../models/entities/user'; | ||||
| import { Blockings } from '../../../models'; | ||||
| import { SelectQueryBuilder } from 'typeorm'; | ||||
| import { Brackets, SelectQueryBuilder } from 'typeorm'; | ||||
| 
 | ||||
| // ここでいうBlockedは被Blockedの意
 | ||||
| export function generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) { | ||||
| 	const blockingQuery = Blockings.createQueryBuilder('blocking') | ||||
| 		.select('blocking.blockerId') | ||||
| 		.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id }); | ||||
| 
 | ||||
| 	// 投稿の作者にブロックされていない かつ
 | ||||
| 	// 投稿の返信先の作者にブロックされていない かつ
 | ||||
| 	// 投稿の引用元の作者にブロックされていない
 | ||||
| 	q | ||||
| 		.andWhere(`note.userId NOT IN (${ blockingQuery.getQuery() })`) | ||||
| 		.andWhere(new Brackets(qb => { qb | ||||
| 			.where(`note.replyUserId IS NULL`) | ||||
| 			.orWhere(`note.replyUserId NOT IN (${ blockingQuery.getQuery() })`); | ||||
| 		})) | ||||
| 		.andWhere(new Brackets(qb => { qb | ||||
| 			.where(`note.renoteUserId IS NULL`) | ||||
| 			.orWhere(`note.renoteUserId NOT IN (${ blockingQuery.getQuery() })`); | ||||
| 		})); | ||||
| 
 | ||||
| 	q.setParameters(blockingQuery.getParameters()); | ||||
| } | ||||
| 
 | ||||
| export function generateBlockQueryForUsers(q: SelectQueryBuilder<any>, me: { id: User['id'] }) { | ||||
| 	const blockingQuery = Blockings.createQueryBuilder('blocking') | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ import { Note } from '../../../models/entities/note'; | |||
| import { User } from '../../../models/entities/user'; | ||||
| import { Notes, UserProfiles, NoteReactions } from '../../../models'; | ||||
| import { generateMutedUserQuery } from './generate-muted-user-query'; | ||||
| import { generateBlockedUserQuery } from './generate-block-query'; | ||||
| 
 | ||||
| // TODO: リアクション、Renote、返信などをしたノートは除外する
 | ||||
| 
 | ||||
|  | @ -29,6 +30,7 @@ export async function injectFeatured(timeline: Note[], user?: User | null) { | |||
| 		query.andWhere('note.userId != :userId', { userId: user.id }); | ||||
| 
 | ||||
| 		generateMutedUserQuery(query, user); | ||||
| 		generateBlockedUserQuery(query, user); | ||||
| 
 | ||||
| 		const reactionQuery = NoteReactions.createQueryBuilder('reaction') | ||||
| 			.select('reaction.noteId') | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query'; | |||
| import { generateVisibilityQuery } from '../../common/generate-visibility-query'; | ||||
| import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; | ||||
| import { ApiError } from '../../error'; | ||||
| import { generateBlockedUserQuery } from '../../common/generate-block-query'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['antennas', 'account', 'notes'], | ||||
|  | @ -77,6 +78,7 @@ export default define(meta, async (ps, user) => { | |||
| 
 | ||||
| 	generateVisibilityQuery(query, user); | ||||
| 	generateMutedUserQuery(query, user); | ||||
| 	generateBlockedUserQuery(query, user); | ||||
| 
 | ||||
| 	const notes = await query | ||||
| 		.take(ps.limit!) | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query'; | |||
| import { generateVisibilityQuery } from '../../common/generate-visibility-query'; | ||||
| import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; | ||||
| import { ApiError } from '../../error'; | ||||
| import { generateBlockedUserQuery } from '../../common/generate-block-query'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['account', 'notes', 'clips'], | ||||
|  | @ -81,6 +82,7 @@ export default define(meta, async (ps, user) => { | |||
| 	if (user) { | ||||
| 		generateVisibilityQuery(query, user); | ||||
| 		generateMutedUserQuery(query, user); | ||||
| 		generateBlockedUserQuery(query, user); | ||||
| 	} | ||||
| 
 | ||||
| 	const notes = await query | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import { ID } from '@/misc/cafy-id'; | |||
| import define from '../../../define'; | ||||
| import { ApiError } from '../../../error'; | ||||
| import { getUser } from '../../../common/getters'; | ||||
| import { MessagingMessages, DriveFiles, UserGroups, UserGroupJoinings } from '../../../../../models'; | ||||
| import { MessagingMessages, DriveFiles, UserGroups, UserGroupJoinings, Blockings } from '../../../../../models'; | ||||
| import { User } from '../../../../../models/entities/user'; | ||||
| import { UserGroup } from '../../../../../models/entities/user-group'; | ||||
| import { createMessage } from '../../../../../services/messages/create'; | ||||
|  | @ -74,7 +74,13 @@ export const meta = { | |||
| 			message: 'Content required. You need to set text or fileId.', | ||||
| 			code: 'CONTENT_REQUIRED', | ||||
| 			id: '25587321-b0e6-449c-9239-f8925092942c' | ||||
| 		} | ||||
| 		}, | ||||
| 
 | ||||
| 		youHaveBeenBlocked: { | ||||
| 			message: 'You cannot send a message because you have been blocked by this user.', | ||||
| 			code: 'YOU_HAVE_BEEN_BLOCKED', | ||||
| 			id: 'c15a5199-7422-4968-941a-2a462c478f7d' | ||||
| 		}, | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
|  | @ -93,6 +99,15 @@ export default define(meta, async (ps, user) => { | |||
| 			if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); | ||||
| 			throw e; | ||||
| 		}); | ||||
| 
 | ||||
| 		// Check blocking
 | ||||
| 		const block = await Blockings.findOne({ | ||||
| 			blockerId: recipientUser.id, | ||||
| 			blockeeId: user.id, | ||||
| 		}); | ||||
| 		if (block) { | ||||
| 			throw new ApiError(meta.errors.youHaveBeenBlocked); | ||||
| 		} | ||||
| 	} else if (ps.groupId != null) { | ||||
| 		// Fetch recipient (group)
 | ||||
| 		recipientGroup = await UserGroups.findOne(ps.groupId); | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import { generateVisibilityQuery } from '../../common/generate-visibility-query' | |||
| import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; | ||||
| import { Brackets } from 'typeorm'; | ||||
| import { Notes } from '../../../../models'; | ||||
| import { generateBlockedUserQuery } from '../../common/generate-block-query'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['notes'], | ||||
|  | @ -63,6 +64,7 @@ export default define(meta, async (ps, user) => { | |||
| 
 | ||||
| 	generateVisibilityQuery(query, user); | ||||
| 	if (user) generateMutedUserQuery(query, user); | ||||
| 	if (user) generateBlockedUserQuery(query, user); | ||||
| 
 | ||||
| 	const notes = await query.take(ps.limit!).getMany(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ import { fetchMeta } from '@/misc/fetch-meta'; | |||
| import { ApiError } from '../../error'; | ||||
| import { ID } from '@/misc/cafy-id'; | ||||
| import { User } from '../../../../models/entities/user'; | ||||
| import { Users, DriveFiles, Notes, Channels } from '../../../../models'; | ||||
| import { Users, DriveFiles, Notes, Channels, Blockings } from '../../../../models'; | ||||
| import { DriveFile } from '../../../../models/entities/drive-file'; | ||||
| import { Note } from '../../../../models/entities/note'; | ||||
| import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits'; | ||||
|  | @ -171,6 +171,12 @@ export const meta = { | |||
| 			code: 'NO_SUCH_CHANNEL', | ||||
| 			id: 'b1653923-5453-4edc-b786-7c4f39bb0bbb' | ||||
| 		}, | ||||
| 
 | ||||
| 		youHaveBeenBlocked: { | ||||
| 			message: 'You have been blocked by this user.', | ||||
| 			code: 'YOU_HAVE_BEEN_BLOCKED', | ||||
| 			id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3' | ||||
| 		}, | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
|  | @ -202,6 +208,17 @@ export default define(meta, async (ps, user) => { | |||
| 		} else if (renote.renoteId && !renote.text && !renote.fileIds) { | ||||
| 			throw new ApiError(meta.errors.cannotReRenote); | ||||
| 		} | ||||
| 
 | ||||
| 		// Check blocking
 | ||||
| 		if (renote.userId !== user.id) { | ||||
| 			const block = await Blockings.findOne({ | ||||
| 				blockerId: renote.userId, | ||||
| 				blockeeId: user.id, | ||||
| 			}); | ||||
| 			if (block) { | ||||
| 				throw new ApiError(meta.errors.youHaveBeenBlocked); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	let reply: Note | undefined; | ||||
|  | @ -217,6 +234,17 @@ export default define(meta, async (ps, user) => { | |||
| 		if (reply.renoteId && !reply.text && !reply.fileIds) { | ||||
| 			throw new ApiError(meta.errors.cannotReplyToPureRenote); | ||||
| 		} | ||||
| 
 | ||||
| 		// Check blocking
 | ||||
| 		if (reply.userId !== user.id) { | ||||
| 			const block = await Blockings.findOne({ | ||||
| 				blockerId: reply.userId, | ||||
| 				blockeeId: user.id, | ||||
| 			}); | ||||
| 			if (block) { | ||||
| 				throw new ApiError(meta.errors.youHaveBeenBlocked); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if (ps.poll) { | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import $ from 'cafy'; | |||
| import define from '../../define'; | ||||
| import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; | ||||
| import { Notes } from '../../../../models'; | ||||
| import { generateBlockedUserQuery } from '../../common/generate-block-query'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['notes'], | ||||
|  | @ -48,6 +49,7 @@ export default define(meta, async (ps, user) => { | |||
| 		.leftJoinAndSelect('renote.user', 'renoteUser'); | ||||
| 
 | ||||
| 	if (user) generateMutedUserQuery(query, user); | ||||
| 	if (user) generateBlockedUserQuery(query, user); | ||||
| 
 | ||||
| 	let notes = await query | ||||
| 		.orderBy('note.score', 'DESC') | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; | |||
| import { activeUsersChart } from '../../../../services/chart'; | ||||
| import { generateRepliesQuery } from '../../common/generate-replies-query'; | ||||
| import { generateMutedNoteQuery } from '../../common/generate-muted-note-query'; | ||||
| import { generateBlockedUserQuery } from '../../common/generate-block-query'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['notes'], | ||||
|  | @ -81,6 +82,7 @@ export default define(meta, async (ps, user) => { | |||
| 	generateRepliesQuery(query, user); | ||||
| 	if (user) generateMutedUserQuery(query, user); | ||||
| 	if (user) generateMutedNoteQuery(query, user); | ||||
| 	if (user) generateBlockedUserQuery(query, user); | ||||
| 
 | ||||
| 	if (ps.withFiles) { | ||||
| 		query.andWhere('note.fileIds != \'{}\''); | ||||
|  |  | |||
|  | @ -12,6 +12,7 @@ import { activeUsersChart } from '../../../../services/chart'; | |||
| import { generateRepliesQuery } from '../../common/generate-replies-query'; | ||||
| import { generateMutedNoteQuery } from '../../common/generate-muted-note-query'; | ||||
| import { generateChannelQuery } from '../../common/generate-channel-query'; | ||||
| import { generateBlockedUserQuery } from '../../common/generate-block-query'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['notes'], | ||||
|  | @ -108,6 +109,7 @@ export default define(meta, async (ps, user) => { | |||
| 	generateVisibilityQuery(query, user); | ||||
| 	generateMutedUserQuery(query, user); | ||||
| 	generateMutedNoteQuery(query, user); | ||||
| 	generateBlockedUserQuery(query, user); | ||||
| 
 | ||||
| 	if (ps.includeMyRenotes === false) { | ||||
| 		query.andWhere(new Brackets(qb => { | ||||
|  |  | |||
|  | @ -12,6 +12,7 @@ import { Brackets } from 'typeorm'; | |||
| import { generateRepliesQuery } from '../../common/generate-replies-query'; | ||||
| import { generateMutedNoteQuery } from '../../common/generate-muted-note-query'; | ||||
| import { generateChannelQuery } from '../../common/generate-channel-query'; | ||||
| import { generateBlockedUserQuery } from '../../common/generate-block-query'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['notes'], | ||||
|  | @ -94,6 +95,7 @@ export default define(meta, async (ps, user) => { | |||
| 	generateVisibilityQuery(query, user); | ||||
| 	if (user) generateMutedUserQuery(query, user); | ||||
| 	if (user) generateMutedNoteQuery(query, user); | ||||
| 	if (user) generateBlockedUserQuery(query, user); | ||||
| 
 | ||||
| 	if (ps.withFiles) { | ||||
| 		query.andWhere('note.fileIds != \'{}\''); | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ import { generateVisibilityQuery } from '../../common/generate-visibility-query' | |||
| import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; | ||||
| import { makePaginationQuery } from '../../common/make-pagination-query'; | ||||
| import { Brackets } from 'typeorm'; | ||||
| import { generateBlockedUserQuery } from '../../common/generate-block-query'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['notes'], | ||||
|  | @ -66,6 +67,7 @@ export default define(meta, async (ps, user) => { | |||
| 
 | ||||
| 	generateVisibilityQuery(query, user); | ||||
| 	generateMutedUserQuery(query, user); | ||||
| 	generateBlockedUserQuery(query, user); | ||||
| 
 | ||||
| 	if (ps.visibility) { | ||||
| 		query.andWhere('note.visibility = :visibility', { visibility: ps.visibility }); | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ import { deliver } from '../../../../../queue'; | |||
| import { renderActivity } from '../../../../../remote/activitypub/renderer'; | ||||
| import renderVote from '../../../../../remote/activitypub/renderer/vote'; | ||||
| import { deliverQuestionUpdate } from '../../../../../services/note/polls/update'; | ||||
| import { PollVotes, NoteWatchings, Users, Polls } from '../../../../../models'; | ||||
| import { PollVotes, NoteWatchings, Users, Polls, Blockings } from '../../../../../models'; | ||||
| import { Not } from 'typeorm'; | ||||
| import { IRemoteUser } from '../../../../../models/entities/user'; | ||||
| import { genId } from '@/misc/gen-id'; | ||||
|  | @ -61,6 +61,12 @@ export const meta = { | |||
| 			code: 'ALREADY_EXPIRED', | ||||
| 			id: '1022a357-b085-4054-9083-8f8de358337e' | ||||
| 		}, | ||||
| 
 | ||||
| 		youHaveBeenBlocked: { | ||||
| 			message: 'You cannot vote this poll because you have been blocked by this user.', | ||||
| 			code: 'YOU_HAVE_BEEN_BLOCKED', | ||||
| 			id: '85a5377e-b1e9-4617-b0b9-5bea73331e49' | ||||
| 		}, | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
|  | @ -77,6 +83,17 @@ export default define(meta, async (ps, user) => { | |||
| 		throw new ApiError(meta.errors.noPoll); | ||||
| 	} | ||||
| 
 | ||||
| 	// Check blocking
 | ||||
| 	if (note.userId !== user.id) { | ||||
| 		const block = await Blockings.findOne({ | ||||
| 			blockerId: note.userId, | ||||
| 			blockeeId: user.id, | ||||
| 		}); | ||||
| 		if (block) { | ||||
| 			throw new ApiError(meta.errors.youHaveBeenBlocked); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	const poll = await Polls.findOneOrFail({ noteId: note.id }); | ||||
| 
 | ||||
| 	if (poll.expiresAt && poll.expiresAt < createdAt) { | ||||
|  | @ -103,13 +120,13 @@ export default define(meta, async (ps, user) => { | |||
| 	} | ||||
| 
 | ||||
| 	// Create vote
 | ||||
| 	const vote = await PollVotes.save({ | ||||
| 	const vote = await PollVotes.insert({ | ||||
| 		id: genId(), | ||||
| 		createdAt, | ||||
| 		noteId: note.id, | ||||
| 		userId: user.id, | ||||
| 		choice: ps.choice | ||||
| 	}); | ||||
| 	}).then(x => PollVotes.findOneOrFail(x.identifiers[0])); | ||||
| 
 | ||||
| 	// Increment votes count
 | ||||
| 	const index = ps.choice + 1; // In SQL, array index is 1 based
 | ||||
|  |  | |||
|  | @ -33,7 +33,13 @@ export const meta = { | |||
| 			message: 'You are already reacting to that note.', | ||||
| 			code: 'ALREADY_REACTED', | ||||
| 			id: '71efcf98-86d6-4e2b-b2ad-9d032369366b' | ||||
| 		} | ||||
| 		}, | ||||
| 
 | ||||
| 		youHaveBeenBlocked: { | ||||
| 			message: 'You cannot react this note because you have been blocked by this user.', | ||||
| 			code: 'YOU_HAVE_BEEN_BLOCKED', | ||||
| 			id: '20ef5475-9f38-4e4c-bd33-de6d979498ec' | ||||
| 		}, | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
|  | @ -44,6 +50,7 @@ export default define(meta, async (ps, user) => { | |||
| 	}); | ||||
| 	await createReaction(user, note, ps.reaction).catch(e => { | ||||
| 		if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted); | ||||
| 		if (e.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked); | ||||
| 		throw e; | ||||
| 	}); | ||||
| 	return; | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ import { generateVisibilityQuery } from '../../common/generate-visibility-query' | |||
| import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; | ||||
| import { makePaginationQuery } from '../../common/make-pagination-query'; | ||||
| import { Notes } from '../../../../models'; | ||||
| import { generateBlockedUserQuery } from '../../common/generate-block-query'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['notes'], | ||||
|  | @ -67,6 +68,7 @@ export default define(meta, async (ps, user) => { | |||
| 
 | ||||
| 	generateVisibilityQuery(query, user); | ||||
| 	if (user) generateMutedUserQuery(query, user); | ||||
| 	if (user) generateBlockedUserQuery(query, user); | ||||
| 
 | ||||
| 	const renotes = await query.take(ps.limit!).getMany(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import { Notes } from '../../../../models'; | |||
| import { makePaginationQuery } from '../../common/make-pagination-query'; | ||||
| import { generateVisibilityQuery } from '../../common/generate-visibility-query'; | ||||
| import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; | ||||
| import { generateBlockedUserQuery } from '../../common/generate-block-query'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['notes'], | ||||
|  | @ -52,6 +53,7 @@ export default define(meta, async (ps, user) => { | |||
| 
 | ||||
| 	generateVisibilityQuery(query, user); | ||||
| 	if (user) generateMutedUserQuery(query, user); | ||||
| 	if (user) generateBlockedUserQuery(query, user); | ||||
| 
 | ||||
| 	const timeline = await query.take(ps.limit!).getMany(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ import { generateVisibilityQuery } from '../../common/generate-visibility-query' | |||
| import { Brackets } from 'typeorm'; | ||||
| import { safeForSql } from '@/misc/safe-for-sql'; | ||||
| import { normalizeForSearch } from '@/misc/normalize-for-search'; | ||||
| import { generateBlockedUserQuery } from '../../common/generate-block-query'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['notes', 'hashtags'], | ||||
|  | @ -75,6 +76,7 @@ export default define(meta, async (ps, me) => { | |||
| 
 | ||||
| 	generateVisibilityQuery(query, me); | ||||
| 	if (me) generateMutedUserQuery(query, me); | ||||
| 	if (me) generateBlockedUserQuery(query, me); | ||||
| 
 | ||||
| 	try { | ||||
| 		if (ps.tag) { | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ import config from '@/config'; | |||
| import { makePaginationQuery } from '../../common/make-pagination-query'; | ||||
| import { generateVisibilityQuery } from '../../common/generate-visibility-query'; | ||||
| import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; | ||||
| import { generateBlockedUserQuery } from '../../common/generate-block-query'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['notes'], | ||||
|  | @ -82,6 +83,7 @@ export default define(meta, async (ps, me) => { | |||
| 
 | ||||
| 		generateVisibilityQuery(query, me); | ||||
| 		if (me) generateMutedUserQuery(query, me); | ||||
| 		if (me) generateBlockedUserQuery(query, me); | ||||
| 
 | ||||
| 		const notes = await query.take(ps.limit!).getMany(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ import { Brackets } from 'typeorm'; | |||
| import { generateRepliesQuery } from '../../common/generate-replies-query'; | ||||
| import { generateMutedNoteQuery } from '../../common/generate-muted-note-query'; | ||||
| import { generateChannelQuery } from '../../common/generate-channel-query'; | ||||
| import { generateBlockedUserQuery } from '../../common/generate-block-query'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['notes'], | ||||
|  | @ -100,6 +101,7 @@ export default define(meta, async (ps, user) => { | |||
| 	generateVisibilityQuery(query, user); | ||||
| 	generateMutedUserQuery(query, user); | ||||
| 	generateMutedNoteQuery(query, user); | ||||
| 	generateBlockedUserQuery(query, user); | ||||
| 
 | ||||
| 	if (ps.includeMyRenotes === false) { | ||||
| 		query.andWhere(new Brackets(qb => { | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import $ from 'cafy'; | |||
| import define from '../define'; | ||||
| import { Users } from '../../../models'; | ||||
| import { generateMutedUserQueryForUsers } from '../common/generate-muted-user-query'; | ||||
| import { generateBlockedUserQuery } from '../common/generate-block-query'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['users'], | ||||
|  | @ -89,6 +90,7 @@ export default define(meta, async (ps, me) => { | |||
| 	} | ||||
| 
 | ||||
| 	if (me) generateMutedUserQueryForUsers(query, me); | ||||
| 	if (me) generateBlockedUserQuery(query, me); | ||||
| 
 | ||||
| 	query.take(ps.limit!); | ||||
| 	query.skip(ps.offset); | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ import define from '../../../define'; | |||
| import { ApiError } from '../../../error'; | ||||
| import { getUser } from '../../../common/getters'; | ||||
| import { pushUserToUserList } from '../../../../../services/user-list/push'; | ||||
| import { UserLists, UserListJoinings } from '../../../../../models'; | ||||
| import { UserLists, UserListJoinings, Blockings } from '../../../../../models'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['lists', 'users'], | ||||
|  | @ -40,7 +40,13 @@ export const meta = { | |||
| 			message: 'That user has already been added to that list.', | ||||
| 			code: 'ALREADY_ADDED', | ||||
| 			id: '1de7c884-1595-49e9-857e-61f12f4d4fc5' | ||||
| 		} | ||||
| 		}, | ||||
| 
 | ||||
| 		youHaveBeenBlocked: { | ||||
| 			message: 'You cannot push this user because you have been blocked by this user.', | ||||
| 			code: 'YOU_HAVE_BEEN_BLOCKED', | ||||
| 			id: '990232c5-3f9d-4d83-9f3f-ef27b6332a4b' | ||||
| 		}, | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
|  | @ -61,6 +67,17 @@ export default define(meta, async (ps, me) => { | |||
| 		throw e; | ||||
| 	}); | ||||
| 
 | ||||
| 	// Check blocking
 | ||||
| 	if (user.id !== me.id) { | ||||
| 		const block = await Blockings.findOne({ | ||||
| 			blockerId: user.id, | ||||
| 			blockeeId: me.id, | ||||
| 		}); | ||||
| 		if (block) { | ||||
| 			throw new ApiError(meta.errors.youHaveBeenBlocked); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	const exist = await UserListJoinings.findOne({ | ||||
| 		userListId: userList.id, | ||||
| 		userId: user.id | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ import { generateVisibilityQuery } from '../../common/generate-visibility-query' | |||
| import { Notes } from '../../../../models'; | ||||
| import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; | ||||
| import { Brackets } from 'typeorm'; | ||||
| import { generateBlockedUserQuery } from '../../common/generate-block-query'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['users', 'notes'], | ||||
|  | @ -100,6 +101,7 @@ export default define(meta, async (ps, me) => { | |||
| 
 | ||||
| 	generateVisibilityQuery(query, me); | ||||
| 	if (me) generateMutedUserQuery(query, me, user); | ||||
| 	if (me) generateBlockedUserQuery(query, me); | ||||
| 
 | ||||
| 	if (ps.withFiles) { | ||||
| 		query.andWhere('note.fileIds != \'{}\''); | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import $ from 'cafy'; | |||
| import define from '../../define'; | ||||
| import { Users, Followings } from '../../../../models'; | ||||
| import { generateMutedUserQueryForUsers } from '../../common/generate-muted-user-query'; | ||||
| import { generateBlockQueryForUsers } from '../../common/generate-block-query'; | ||||
| import { generateBlockedUserQuery, generateBlockQueryForUsers } from '../../common/generate-block-query'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['users'], | ||||
|  | @ -46,6 +46,7 @@ export default define(meta, async (ps, me) => { | |||
| 
 | ||||
| 	generateMutedUserQueryForUsers(query, me); | ||||
| 	generateBlockQueryForUsers(query, me); | ||||
| 	generateBlockedUserQuery(query, me); | ||||
| 
 | ||||
| 	const followingQuery = Followings.createQueryBuilder('following') | ||||
| 		.select('following.followeeId') | ||||
|  |  | |||
|  | @ -27,6 +27,10 @@ export default abstract class Channel { | |||
| 		return this.connection.muting; | ||||
| 	} | ||||
| 
 | ||||
| 	protected get blocking() { | ||||
| 		return this.connection.blocking; | ||||
| 	} | ||||
| 
 | ||||
| 	protected get followingChannels() { | ||||
| 		return this.connection.followingChannels; | ||||
| 	} | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import autobind from 'autobind-decorator'; | |||
| import Channel from '../channel'; | ||||
| import { Notes } from '../../../../models'; | ||||
| import { isMutedUserRelated } from '@/misc/is-muted-user-related'; | ||||
| import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	public readonly chName = 'antenna'; | ||||
|  | @ -26,6 +27,8 @@ export default class extends Channel { | |||
| 
 | ||||
| 			// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 | ||||
| 			if (isMutedUserRelated(note, this.muting)) return; | ||||
| 			// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
 | ||||
| 			if (isBlockerUserRelated(note, this.blocking)) return; | ||||
| 
 | ||||
| 			this.connection.cacheNote(note); | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import autobind from 'autobind-decorator'; | |||
| import Channel from '../channel'; | ||||
| import { Notes, Users } from '../../../../models'; | ||||
| import { isMutedUserRelated } from '@/misc/is-muted-user-related'; | ||||
| import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; | ||||
| import { PackedNote } from '../../../../models/repositories/note'; | ||||
| import { User } from '../../../../models/entities/user'; | ||||
| 
 | ||||
|  | @ -42,6 +43,8 @@ export default class extends Channel { | |||
| 
 | ||||
| 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 | ||||
| 		if (isMutedUserRelated(note, this.muting)) return; | ||||
| 		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
 | ||||
| 		if (isBlockerUserRelated(note, this.blocking)) return; | ||||
| 
 | ||||
| 		this.connection.cacheNote(note); | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import { fetchMeta } from '@/misc/fetch-meta'; | |||
| import { Notes } from '../../../../models'; | ||||
| import { PackedNote } from '../../../../models/repositories/note'; | ||||
| import { checkWordMute } from '@/misc/check-word-mute'; | ||||
| import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	public readonly chName = 'globalTimeline'; | ||||
|  | @ -49,6 +50,8 @@ export default class extends Channel { | |||
| 
 | ||||
| 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 | ||||
| 		if (isMutedUserRelated(note, this.muting)) return; | ||||
| 		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
 | ||||
| 		if (isBlockerUserRelated(note, this.blocking)) return; | ||||
| 
 | ||||
| 		// 流れてきたNoteがミュートすべきNoteだったら無視する
 | ||||
| 		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
 | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import Channel from '../channel'; | |||
| import { Notes } from '../../../../models'; | ||||
| import { PackedNote } from '../../../../models/repositories/note'; | ||||
| import { normalizeForSearch } from '@/misc/normalize-for-search'; | ||||
| import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	public readonly chName = 'hashtag'; | ||||
|  | @ -36,6 +37,8 @@ export default class extends Channel { | |||
| 
 | ||||
| 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 | ||||
| 		if (isMutedUserRelated(note, this.muting)) return; | ||||
| 		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
 | ||||
| 		if (isBlockerUserRelated(note, this.blocking)) return; | ||||
| 
 | ||||
| 		this.connection.cacheNote(note); | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import Channel from '../channel'; | |||
| import { Notes } from '../../../../models'; | ||||
| import { PackedNote } from '../../../../models/repositories/note'; | ||||
| import { checkWordMute } from '@/misc/check-word-mute'; | ||||
| import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	public readonly chName = 'homeTimeline'; | ||||
|  | @ -57,6 +58,8 @@ export default class extends Channel { | |||
| 
 | ||||
| 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 | ||||
| 		if (isMutedUserRelated(note, this.muting)) return; | ||||
| 		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
 | ||||
| 		if (isBlockerUserRelated(note, this.blocking)) return; | ||||
| 
 | ||||
| 		// 流れてきたNoteがミュートすべきNoteだったら無視する
 | ||||
| 		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
 | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import { Notes } from '../../../../models'; | |||
| import { PackedNote } from '../../../../models/repositories/note'; | ||||
| import { PackedUser } from '../../../../models/repositories/user'; | ||||
| import { checkWordMute } from '@/misc/check-word-mute'; | ||||
| import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	public readonly chName = 'hybridTimeline'; | ||||
|  | @ -66,6 +67,8 @@ export default class extends Channel { | |||
| 
 | ||||
| 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 | ||||
| 		if (isMutedUserRelated(note, this.muting)) return; | ||||
| 		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
 | ||||
| 		if (isBlockerUserRelated(note, this.blocking)) return; | ||||
| 
 | ||||
| 		// 流れてきたNoteがミュートすべきNoteだったら無視する
 | ||||
| 		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
 | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import { Notes } from '../../../../models'; | |||
| import { PackedNote } from '../../../../models/repositories/note'; | ||||
| import { PackedUser } from '../../../../models/repositories/user'; | ||||
| import { checkWordMute } from '@/misc/check-word-mute'; | ||||
| import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	public readonly chName = 'localTimeline'; | ||||
|  | @ -51,6 +52,8 @@ export default class extends Channel { | |||
| 
 | ||||
| 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 | ||||
| 		if (isMutedUserRelated(note, this.muting)) return; | ||||
| 		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
 | ||||
| 		if (isBlockerUserRelated(note, this.blocking)) return; | ||||
| 
 | ||||
| 		// 流れてきたNoteがミュートすべきNoteだったら無視する
 | ||||
| 		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
 | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import { Notes, UserListJoinings, UserLists } from '../../../../models'; | |||
| import { isMutedUserRelated } from '@/misc/is-muted-user-related'; | ||||
| import { User } from '../../../../models/entities/user'; | ||||
| import { PackedNote } from '../../../../models/repositories/note'; | ||||
| import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	public readonly chName = 'userList'; | ||||
|  | @ -74,6 +75,8 @@ export default class extends Channel { | |||
| 
 | ||||
| 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 | ||||
| 		if (isMutedUserRelated(note, this.muting)) return; | ||||
| 		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
 | ||||
| 		if (isBlockerUserRelated(note, this.blocking)) return; | ||||
| 
 | ||||
| 		this.send('note', note); | ||||
| 	} | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ import channels from './channels'; | |||
| import { EventEmitter } from 'events'; | ||||
| import { User } from '../../../models/entities/user'; | ||||
| import { Channel as ChannelModel } from '../../../models/entities/channel'; | ||||
| import { Users, Followings, Mutings, UserProfiles, ChannelFollowings } from '../../../models'; | ||||
| import { Users, Followings, Mutings, UserProfiles, ChannelFollowings, Blockings } from '../../../models'; | ||||
| import { ApiError } from '../error'; | ||||
| import { AccessToken } from '../../../models/entities/access-token'; | ||||
| import { UserProfile } from '../../../models/entities/user-profile'; | ||||
|  | @ -24,6 +24,7 @@ export default class Connection { | |||
| 	public userProfile?: UserProfile; | ||||
| 	public following: Set<User['id']> = new Set(); | ||||
| 	public muting: Set<User['id']> = new Set(); | ||||
| 	public blocking: Set<User['id']> = new Set(); // "被"blocking
 | ||||
| 	public followingChannels: Set<ChannelModel['id']> = new Set(); | ||||
| 	public token?: AccessToken; | ||||
| 	private wsConnection: websocket.connection; | ||||
|  | @ -52,6 +53,7 @@ export default class Connection { | |||
| 		if (this.user) { | ||||
| 			this.updateFollowing(); | ||||
| 			this.updateMuting(); | ||||
| 			this.updateBlocking(); | ||||
| 			this.updateFollowingChannels(); | ||||
| 			this.updateUserProfile(); | ||||
| 
 | ||||
|  | @ -80,6 +82,8 @@ export default class Connection { | |||
| 				this.muting.delete(body.id); | ||||
| 				break; | ||||
| 
 | ||||
| 			// TODO: block events
 | ||||
| 
 | ||||
| 			case 'followChannel': | ||||
| 				this.followingChannels.add(body.id); | ||||
| 				break; | ||||
|  | @ -375,6 +379,18 @@ export default class Connection { | |||
| 		this.muting = new Set<string>(mutings.map(x => x.muteeId)); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	private async updateBlocking() { // ここでいうBlockingは被Blockingの意
 | ||||
| 		const blockings = await Blockings.find({ | ||||
| 			where: { | ||||
| 				blockeeId: this.user!.id | ||||
| 			}, | ||||
| 			select: ['blockerId'] | ||||
| 		}); | ||||
| 
 | ||||
| 		this.blocking = new Set<string>(blockings.map(x => x.blockerId)); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	private async updateFollowingChannels() { | ||||
| 		const followings = await ChannelFollowings.find({ | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ import renderBlock from '../../remote/activitypub/renderer/block'; | |||
| import { deliver } from '../../queue'; | ||||
| import renderReject from '../../remote/activitypub/renderer/reject'; | ||||
| import { User } from '../../models/entities/user'; | ||||
| import { Blockings, Users, FollowRequests, Followings } from '../../models'; | ||||
| import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLists } from '../../models'; | ||||
| import { perUserFollowingChart } from '../chart'; | ||||
| import { genId } from '@/misc/gen-id'; | ||||
| 
 | ||||
|  | @ -15,7 +15,8 @@ export default async function(blocker: User, blockee: User) { | |||
| 		cancelRequest(blocker, blockee), | ||||
| 		cancelRequest(blockee, blocker), | ||||
| 		unFollow(blocker, blockee), | ||||
| 		unFollow(blockee, blocker) | ||||
| 		unFollow(blockee, blocker), | ||||
| 		removeFromList(blockee, blocker), | ||||
| 	]); | ||||
| 
 | ||||
| 	await Blockings.insert({ | ||||
|  | @ -112,3 +113,16 @@ async function unFollow(follower: User, followee: User) { | |||
| 		deliver(follower, content, followee.inbox); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| async function removeFromList(listOwner: User, user: User) { | ||||
| 	const userLists = await UserLists.find({ | ||||
| 		userId: listOwner.id, | ||||
| 	}); | ||||
| 
 | ||||
| 	for (const userList of userLists) { | ||||
| 		await UserListJoinings.delete({ | ||||
| 			userListId: userList.id, | ||||
| 			userId: user.id, | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ import { extractMentions } from '@/misc/extract-mentions'; | |||
| import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm'; | ||||
| import { extractHashtags } from '@/misc/extract-hashtags'; | ||||
| import { Note, IMentionedRemoteUsers } from '../../models/entities/note'; | ||||
| import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings, MutedNotes, Channels, ChannelFollowings } from '../../models'; | ||||
| import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings, MutedNotes, Channels, ChannelFollowings, Blockings } from '../../models'; | ||||
| import { DriveFile } from '../../models/entities/drive-file'; | ||||
| import { App } from '../../models/entities/app'; | ||||
| import { Not, getConnection, In } from 'typeorm'; | ||||
|  | @ -265,8 +265,10 @@ export default async (user: { id: User['id']; username: User['username']; host: | |||
| 		.andWhere(`following.followeeId = :userId`, { userId: note.userId }) | ||||
| 		.getMany() | ||||
| 		.then(async followings => { | ||||
| 			const blockings = await Blockings.find({ blockerId: user.id }); // TODO: キャッシュしたい
 | ||||
| 			const followers = followings.map(f => f.followerId); | ||||
| 			for (const antenna of (await getAntennas())) { | ||||
| 				if (blockings.some(blocking => blocking.blockeeId === antenna.userId)) continue; // この処理は checkHitAntenna 内でやるようにしてもいいかも
 | ||||
| 				checkHitAntenna(antenna, note, user, followers).then(hit => { | ||||
| 					if (hit) { | ||||
| 						addNoteToAntenna(antenna, note, user); | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { publishNoteStream } from '../../stream'; | ||||
| import { User } from '../../../models/entities/user'; | ||||
| import { Note } from '../../../models/entities/note'; | ||||
| import { PollVotes, NoteWatchings, Polls } from '../../../models'; | ||||
| import { PollVotes, NoteWatchings, Polls, Blockings } from '../../../models'; | ||||
| import { Not } from 'typeorm'; | ||||
| import { genId } from '@/misc/gen-id'; | ||||
| import { createNotification } from '../../create-notification'; | ||||
|  | @ -14,6 +14,17 @@ export default async function(user: User, note: Note, choice: number) { | |||
| 	// Check whether is valid choice
 | ||||
| 	if (poll.choices[choice] == null) throw new Error('invalid choice param'); | ||||
| 
 | ||||
| 	// Check blocking
 | ||||
| 	if (note.userId !== user.id) { | ||||
| 		const block = await Blockings.findOne({ | ||||
| 			blockerId: note.userId, | ||||
| 			blockeeId: user.id, | ||||
| 		}); | ||||
| 		if (block) { | ||||
| 			throw new Error('blocked'); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// if already voted
 | ||||
| 	const exist = await PollVotes.find({ | ||||
| 		noteId: note.id, | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ import { renderActivity } from '../../../remote/activitypub/renderer'; | |||
| import { toDbReaction, decodeReaction } from '@/misc/reaction-lib'; | ||||
| import { User, IRemoteUser } from '../../../models/entities/user'; | ||||
| import { Note } from '../../../models/entities/note'; | ||||
| import { NoteReactions, Users, NoteWatchings, Notes, Emojis } from '../../../models'; | ||||
| import { NoteReactions, Users, NoteWatchings, Notes, Emojis, Blockings } from '../../../models'; | ||||
| import { Not } from 'typeorm'; | ||||
| import { perUserReactionsChart } from '../../chart'; | ||||
| import { genId } from '@/misc/gen-id'; | ||||
|  | @ -16,6 +16,17 @@ import { NoteReaction } from '../../../models/entities/note-reaction'; | |||
| import { IdentifiableError } from '@/misc/identifiable-error'; | ||||
| 
 | ||||
| export default async (user: { id: User['id']; host: User['host']; }, note: Note, reaction?: string) => { | ||||
| 	// Check blocking
 | ||||
| 	if (note.userId !== user.id) { | ||||
| 		const block = await Blockings.findOne({ | ||||
| 			blockerId: note.userId, | ||||
| 			blockeeId: user.id, | ||||
| 		}); | ||||
| 		if (block) { | ||||
| 			throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7'); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO: cache
 | ||||
| 	reaction = await toDbReaction(reaction, user.host); | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										95
									
								
								test/block.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								test/block.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,95 @@ | |||
| /* | ||||
|  * Tests of block | ||||
|  * | ||||
|  * How to run the tests: | ||||
|  * > npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true npx mocha test/block.ts --require ts-node/register | ||||
|  * | ||||
|  * To specify test: | ||||
|  * > npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true npx mocha test/block.ts --require ts-node/register -g 'test name' | ||||
|  */ | ||||
| 
 | ||||
| process.env.NODE_ENV = 'test'; | ||||
| 
 | ||||
| import * as assert from 'assert'; | ||||
| import * as childProcess from 'child_process'; | ||||
| import { async, signup, request, post, startServer, shutdownServer } from './utils'; | ||||
| 
 | ||||
| describe('Block', () => { | ||||
| 	let p: childProcess.ChildProcess; | ||||
| 
 | ||||
| 	// alice blocks bob
 | ||||
| 	let alice: any; | ||||
| 	let bob: any; | ||||
| 	let carol: any; | ||||
| 
 | ||||
| 	before(async () => { | ||||
| 		p = await startServer(); | ||||
| 		alice = await signup({ username: 'alice' }); | ||||
| 		bob = await signup({ username: 'bob' }); | ||||
| 		carol = await signup({ username: 'carol' }); | ||||
| 	}); | ||||
| 
 | ||||
| 	after(async () => { | ||||
| 		await shutdownServer(p); | ||||
| 	}); | ||||
| 
 | ||||
| 	it('Block作成', async(async () => { | ||||
| 		const res = await request('/blocking/create', { | ||||
| 			userId: bob.id | ||||
| 		}, alice); | ||||
| 
 | ||||
| 		assert.strictEqual(res.status, 200); | ||||
| 	})); | ||||
| 
 | ||||
| 	it('ブロックされているユーザーをフォローできない', async(async () => { | ||||
| 		const res = await request('/following/create', { userId: alice.id }, bob); | ||||
| 
 | ||||
| 		assert.strictEqual(res.status, 400); | ||||
| 		assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'); | ||||
| 	})); | ||||
| 
 | ||||
| 	it('ブロックされているユーザーにリアクションできない', async(async () => { | ||||
| 		const note = await post(alice, { text: 'hello' }); | ||||
| 
 | ||||
| 		const res = await request('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob); | ||||
| 
 | ||||
| 		assert.strictEqual(res.status, 400); | ||||
| 		assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec'); | ||||
| 	})); | ||||
| 
 | ||||
| 	it('ブロックされているユーザーに返信できない', async(async () => { | ||||
| 		const note = await post(alice, { text: 'hello' }); | ||||
| 
 | ||||
| 		const res = await request('/notes/create', { replyId: note.id, text: 'yo' }, bob); | ||||
| 
 | ||||
| 		assert.strictEqual(res.status, 400); | ||||
| 		assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); | ||||
| 	})); | ||||
| 
 | ||||
| 	it('ブロックされているユーザーのノートをRenoteできない', async(async () => { | ||||
| 		const note = await post(alice, { text: 'hello' }); | ||||
| 
 | ||||
| 		const res = await request('/notes/create', { renoteId: note.id, text: 'yo' }, bob); | ||||
| 
 | ||||
| 		assert.strictEqual(res.status, 400); | ||||
| 		assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); | ||||
| 	})); | ||||
| 
 | ||||
| 	// TODO: ユーザーリストに入れられないテスト
 | ||||
| 
 | ||||
| 	// TODO: ユーザーリストから除外されるテスト
 | ||||
| 
 | ||||
| 	it('タイムライン(LTL)にブロックされているユーザーの投稿が含まれない', async(async () => { | ||||
| 		const aliceNote = await post(alice); | ||||
| 		const bobNote = await post(bob); | ||||
| 		const carolNote = await post(carol); | ||||
| 
 | ||||
| 		const res = await request('/notes/local-timeline', {}, bob); | ||||
| 
 | ||||
| 		assert.strictEqual(res.status, 200); | ||||
| 		assert.strictEqual(Array.isArray(res.body), true); | ||||
| 		assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false); | ||||
| 		assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); | ||||
| 		assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); | ||||
| 	})); | ||||
| }); | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue