feat: Undo Accept (#7980)
* allow breaking of follow * send undo * delete by using reject follow
This commit is contained in:
		
							parent
							
								
									a82ff360c6
								
							
						
					
					
						commit
						f33ded3107
					
				
					 6 changed files with 136 additions and 1 deletions
				
			
		|  | @ -792,6 +792,7 @@ pubSub: "Pub/Subのアカウント" | ||||||
| lastCommunication: "直近の通信" | lastCommunication: "直近の通信" | ||||||
| resolved: "解決済み" | resolved: "解決済み" | ||||||
| unresolved: "未解決" | unresolved: "未解決" | ||||||
|  | breakFollow: "フォロワーを解除" | ||||||
| itsOn: "オンになっています" | itsOn: "オンになっています" | ||||||
| itsOff: "オフになっています" | itsOff: "オフになっています" | ||||||
| emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする" | emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする" | ||||||
|  |  | ||||||
|  | @ -0,0 +1,27 @@ | ||||||
|  | import unfollow from '@/services/following/delete'; | ||||||
|  | import cancelRequest from '@/services/following/requests/cancel'; | ||||||
|  | import {IAccept} from '../../type'; | ||||||
|  | import { IRemoteUser } from '@/models/entities/user'; | ||||||
|  | import { Followings } from '@/models/index'; | ||||||
|  | import DbResolver from '../../db-resolver'; | ||||||
|  | 
 | ||||||
|  | export default async (actor: IRemoteUser, activity: IAccept): Promise<string> => { | ||||||
|  | 	const dbResolver = new DbResolver(); | ||||||
|  | 
 | ||||||
|  | 	const follower = await dbResolver.getUserFromApId(activity.object); | ||||||
|  | 	if (follower == null) { | ||||||
|  | 		return `skip: follower not found`; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	const following = await Followings.findOne({ | ||||||
|  | 		followerId: follower.id, | ||||||
|  | 		followeeId: actor.id | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	if (following) { | ||||||
|  | 		await unfollow(follower, actor); | ||||||
|  | 		return `ok: unfollowed`; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return `skip: フォローされていない`; | ||||||
|  | }; | ||||||
|  | @ -1,8 +1,9 @@ | ||||||
| import { IRemoteUser } from '@/models/entities/user'; | import { IRemoteUser } from '@/models/entities/user'; | ||||||
| import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType } from '../../type'; | import {IUndo, isFollow, isBlock, isLike, isAnnounce, getApType, isAccept} from '../../type'; | ||||||
| import unfollow from './follow'; | import unfollow from './follow'; | ||||||
| import unblock from './block'; | import unblock from './block'; | ||||||
| import undoLike from './like'; | import undoLike from './like'; | ||||||
|  | import undoAccept from './accept'; | ||||||
| import { undoAnnounce } from './announce'; | import { undoAnnounce } from './announce'; | ||||||
| import Resolver from '../../resolver'; | import Resolver from '../../resolver'; | ||||||
| import { apLogger } from '../../logger'; | import { apLogger } from '../../logger'; | ||||||
|  | @ -29,6 +30,7 @@ export default async (actor: IRemoteUser, activity: IUndo): Promise<string> => { | ||||||
| 	if (isBlock(object)) return await unblock(actor, object); | 	if (isBlock(object)) return await unblock(actor, object); | ||||||
| 	if (isLike(object)) return await undoLike(actor, object); | 	if (isLike(object)) return await undoLike(actor, object); | ||||||
| 	if (isAnnounce(object)) return await undoAnnounce(actor, object); | 	if (isAnnounce(object)) return await undoAnnounce(actor, object); | ||||||
|  | 	if (isAccept(object)) return await undoAccept(actor, object); | ||||||
| 
 | 
 | ||||||
| 	return `skip: unknown object type ${getApType(object)}`; | 	return `skip: unknown object type ${getApType(object)}`; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,82 @@ | ||||||
|  | import $ from 'cafy'; | ||||||
|  | import { ID } from '@/misc/cafy-id'; | ||||||
|  | import * as ms from 'ms'; | ||||||
|  | import deleteFollowing from '@/services/following/delete'; | ||||||
|  | import define from '../../define'; | ||||||
|  | import { ApiError } from '../../error'; | ||||||
|  | import { getUser } from '../../common/getters'; | ||||||
|  | import { Followings, Users } from '@/models/index'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	tags: ['following', 'users'], | ||||||
|  | 
 | ||||||
|  | 	limit: { | ||||||
|  | 		duration: ms('1hour'), | ||||||
|  | 		max: 100 | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true as const, | ||||||
|  | 
 | ||||||
|  | 	kind: 'write:following', | ||||||
|  | 
 | ||||||
|  | 	params: { | ||||||
|  | 		userId: { | ||||||
|  | 			validator: $.type(ID), | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	errors: { | ||||||
|  | 		noSuchUser: { | ||||||
|  | 			message: 'No such user.', | ||||||
|  | 			code: 'NO_SUCH_USER', | ||||||
|  | 			id: '5b12c78d-2b28-4dca-99d2-f56139b42ff8' | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		followerIsYourself: { | ||||||
|  | 			message: 'Follower is yourself.', | ||||||
|  | 			code: 'FOLLOWER_IS_YOURSELF', | ||||||
|  | 			id: '07dc03b9-03da-422d-885b-438313707662' | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		notFollowing: { | ||||||
|  | 			message: 'The other use is not following you.', | ||||||
|  | 			code: 'NOT_FOLLOWING', | ||||||
|  | 			id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09' | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	res: { | ||||||
|  | 		type: 'object' as const, | ||||||
|  | 		optional: false as const, nullable: false as const, | ||||||
|  | 		ref: 'User' | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default define(meta, async (ps, user) => { | ||||||
|  | 	const followee = user; | ||||||
|  | 
 | ||||||
|  | 	// Check if the follower is yourself
 | ||||||
|  | 	if (user.id === ps.userId) { | ||||||
|  | 		throw new ApiError(meta.errors.followerIsYourself); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Get follower
 | ||||||
|  | 	const follower = await getUser(ps.userId).catch(e => { | ||||||
|  | 		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); | ||||||
|  | 		throw e; | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	// Check not following
 | ||||||
|  | 	const exist = await Followings.findOne({ | ||||||
|  | 		followerId: follower.id, | ||||||
|  | 		followeeId: followee.id | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	if (exist == null) { | ||||||
|  | 		throw new ApiError(meta.errors.notFollowing); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	await deleteFollowing(follower, followee); | ||||||
|  | 
 | ||||||
|  | 	return await Users.pack(followee.id, user); | ||||||
|  | }); | ||||||
|  | @ -2,6 +2,7 @@ import { publishMainStream, publishUserEvent } from '@/services/stream'; | ||||||
| import { renderActivity } from '@/remote/activitypub/renderer/index'; | import { renderActivity } from '@/remote/activitypub/renderer/index'; | ||||||
| import renderFollow from '@/remote/activitypub/renderer/follow'; | import renderFollow from '@/remote/activitypub/renderer/follow'; | ||||||
| import renderUndo from '@/remote/activitypub/renderer/undo'; | import renderUndo from '@/remote/activitypub/renderer/undo'; | ||||||
|  | import renderReject from '@/remote/activitypub/renderer/reject'; | ||||||
| import { deliver } from '@/queue/index'; | import { deliver } from '@/queue/index'; | ||||||
| import Logger from '../logger'; | import Logger from '../logger'; | ||||||
| import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc'; | import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc'; | ||||||
|  | @ -40,6 +41,12 @@ export default async function(follower: { id: User['id']; host: User['host']; ur | ||||||
| 		const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); | 		const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); | ||||||
| 		deliver(follower, content, followee.inbox); | 		deliver(follower, content, followee.inbox); | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	if (Users.isLocalUser(followee) && Users.isRemoteUser(follower)) { | ||||||
|  | 		// local user has null host
 | ||||||
|  | 		const content = renderActivity(renderReject(renderFollow(follower, followee), followee)); | ||||||
|  | 		deliver(followee, content, follower.inbox); | ||||||
|  | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function decrementFollowing(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }) { | export async function decrementFollowing(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }) { | ||||||
|  |  | ||||||
|  | @ -109,6 +109,14 @@ export function getUserMenu(user) { | ||||||
| 		return !confirm.canceled; | 		return !confirm.canceled; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	async function invalidateFollow() { | ||||||
|  | 		os.apiWithDialog('following/invalidate', { | ||||||
|  | 			userId: user.id | ||||||
|  | 		}).then(() => { | ||||||
|  | 			user.isFollowed = !user.isFollowed; | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	let menu = [{ | 	let menu = [{ | ||||||
| 		icon: 'fas fa-at', | 		icon: 'fas fa-at', | ||||||
| 		text: i18n.locale.copyUsername, | 		text: i18n.locale.copyUsername, | ||||||
|  | @ -153,6 +161,14 @@ export function getUserMenu(user) { | ||||||
| 			action: toggleBlock | 			action: toggleBlock | ||||||
| 		}]); | 		}]); | ||||||
| 
 | 
 | ||||||
|  | 		if (user.isFollowed) { | ||||||
|  | 			menu = menu.concat([{ | ||||||
|  | 				icon: 'fas fa-unlink', | ||||||
|  | 				text: i18n.locale.breakFollow, | ||||||
|  | 				action: invalidateFollow | ||||||
|  | 			}]); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		menu = menu.concat([null, { | 		menu = menu.concat([null, { | ||||||
| 			icon: 'fas fa-exclamation-circle', | 			icon: 'fas fa-exclamation-circle', | ||||||
| 			text: i18n.locale.reportAbuse, | 			text: i18n.locale.reportAbuse, | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue