mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 02:55:58 +01:00 
			
		
		
		
	feat: follow/unfollow logic and receipt
This commit is contained in:
		| @@ -3,9 +3,11 @@ | ||||
| const request = require('request-promise-native'); | ||||
| const { generateKeyPairSync } = require('crypto'); | ||||
| const winston = require('winston'); | ||||
| const nconf = require('nconf'); | ||||
|  | ||||
| const db = require('../database'); | ||||
| const ttl = require('../cache/ttl'); | ||||
| const user = require('../user'); | ||||
|  | ||||
| const webfingerCache = ttl({ ttl: 1000 * 60 * 60 * 24 }); // 24 hours | ||||
|  | ||||
| @@ -65,3 +67,13 @@ Helpers.generateKeys = async (uid) => { | ||||
| 	await db.setObject(`uid:${uid}:keys`, { publicKey, privateKey }); | ||||
| 	return { publicKey, privateKey }; | ||||
| }; | ||||
|  | ||||
| Helpers.resolveLocalUid = async (id) => { | ||||
| 	const [slug, host] = id.split('@'); | ||||
|  | ||||
| 	if (id.indexOf('@') === -1 || host !== nconf.get('url_parsed').host) { | ||||
| 		throw new Error('[[activitypub:invalid-id]]'); | ||||
| 	} | ||||
|  | ||||
| 	return await user.getUidByUserslug(slug); | ||||
| }; | ||||
|   | ||||
							
								
								
									
										61
									
								
								src/activitypub/inbox.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/activitypub/inbox.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const db = require('../database'); | ||||
| const user = require('../user'); | ||||
|  | ||||
| const helpers = require('./helpers'); | ||||
|  | ||||
| const inbox = module.exports; | ||||
|  | ||||
| inbox.follow = async (actorId, objectId) => { | ||||
| 	await handleFollow('follow', actorId, objectId); | ||||
| }; | ||||
|  | ||||
| inbox.unfollow = async (actorId, objectId) => { | ||||
| 	await handleFollow('unfollow', actorId, objectId); | ||||
| }; | ||||
|  | ||||
| inbox.isFollowed = async (actorId, uid) => { | ||||
| 	if (actorId.indexOf('@') === -1 || parseInt(uid, 10) <= 0) { | ||||
| 		return false; | ||||
| 	} | ||||
| 	return await db.isSortedSetMember(`followersRemote:${uid}`, actorId); | ||||
| }; | ||||
|  | ||||
| async function handleFollow(type, actorId, objectId) { | ||||
| 	// Sanity checks | ||||
| 	const actorExists = await helpers.query(actorId); | ||||
| 	if (!actorId || !actorExists) { | ||||
| 		throw new Error('[[error:invalid-uid]]'); // should probably be AP specific | ||||
| 	} | ||||
|  | ||||
| 	if (!objectId) { | ||||
| 		throw new Error('[[error:invalid-uid]]'); // should probably be AP specific | ||||
| 	} | ||||
|  | ||||
| 	const localUid = await helpers.resolveLocalUid(objectId); | ||||
| 	if (!localUid) { | ||||
| 		throw new Error('[[error:invalid-uid]]'); | ||||
| 	} | ||||
|  | ||||
| 	// matches toggleFollow() in src/user/follow.js | ||||
| 	const isFollowed = await inbox.isFollowed(actorId, localUid); | ||||
| 	if (type === 'follow') { | ||||
| 		if (isFollowed) { | ||||
| 			throw new Error('[[error:already-following]]'); | ||||
| 		} | ||||
| 		const now = Date.now(); | ||||
| 		await db.sortedSetAdd(`followersRemote:${localUid}`, now, actorId); | ||||
| 	} else { | ||||
| 		if (!isFollowed) { | ||||
| 			throw new Error('[[error:not-following]]'); | ||||
| 		} | ||||
| 		await db.sortedSetRemove(`followersRemote:${localUid}`, actorId); | ||||
| 	} | ||||
|  | ||||
| 	const [followerCount, followerRemoteCount] = await Promise.all([ | ||||
| 		db.sortedSetCard(`followers:${localUid}`), | ||||
| 		db.sortedSetCard(`followersRemote:${localUid}`), | ||||
| 	]); | ||||
| 	await user.setUserField(localUid, 'followerCount', followerCount + followerRemoteCount); | ||||
| } | ||||
| @@ -12,6 +12,8 @@ const actorCache = ttl({ ttl: 1000 * 60 * 60 * 24 }); // 24 hours | ||||
| const ActivityPub = module.exports; | ||||
|  | ||||
| ActivityPub.helpers = require('./helpers'); | ||||
| ActivityPub.inbox = require('./inbox'); | ||||
| ActivityPub.outbox = require('./outbox'); | ||||
|  | ||||
| ActivityPub.getActor = async (id) => { | ||||
| 	if (actorCache.has(id)) { | ||||
|   | ||||
							
								
								
									
										13
									
								
								src/activitypub/outbox.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/activitypub/outbox.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const db = require('../database'); | ||||
|  | ||||
| const outbox = module.exports; | ||||
|  | ||||
| outbox.isFollowing = async (uid, actorId) => { | ||||
| 	if (parseInt(uid, 10) <= 0 || actorId.indexOf('@') === -1) { | ||||
| 		return false; | ||||
| 	} | ||||
| 	return await db.isSortedSetMember(`followingRemote:${uid}`, actorId); | ||||
| }; | ||||
|  | ||||
| @@ -2,8 +2,10 @@ | ||||
|  | ||||
| const nconf = require('nconf'); | ||||
|  | ||||
| const db = require('../../database'); | ||||
| const user = require('../../user'); | ||||
| const activitypub = require('../../activitypub'); | ||||
| const helpers = require('../helpers'); | ||||
|  | ||||
| const Controller = module.exports; | ||||
|  | ||||
| @@ -99,7 +101,17 @@ Controller.getInbox = async (req, res) => { | ||||
| }; | ||||
|  | ||||
| Controller.postInbox = async (req, res) => { | ||||
| 	console.log('received', req.body); | ||||
| 	switch (req.body.type) { | ||||
| 		case 'Follow': { | ||||
| 			await activitypub.inbox.follow(req.body.actor.name, req.body.object.name); | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| 		case 'Unfollow': { | ||||
| 			await activitypub.inbox.unfollow(req.body.actor.name, req.body.object.name); | ||||
| 			break; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	res.sendStatus(201); | ||||
| }; | ||||
| @@ -109,18 +121,50 @@ Controller.postInbox = async (req, res) => { | ||||
|  */ | ||||
|  | ||||
| Controller.follow = async (req, res) => { | ||||
| 	await activitypub.send(req.uid, req.params.uid, { | ||||
| 		type: 'Follow', | ||||
| 		object: { | ||||
| 			type: 'Person', | ||||
| 			name: req.params.uid, | ||||
| 		}, | ||||
| 	}); | ||||
| 	try { | ||||
| 		const { uid: objectId } = req.params; | ||||
| 		await activitypub.send(req.uid, objectId, { | ||||
| 			type: 'Follow', | ||||
| 			object: { | ||||
| 				type: 'Person', | ||||
| 				name: objectId, | ||||
| 			}, | ||||
| 		}); | ||||
|  | ||||
| 	res.sendStatus(201); | ||||
| 		const now = Date.now(); | ||||
| 		await db.sortedSetAdd(`followingRemote:${req.uid}`, now, objectId); | ||||
| 		await recountFollowing(req.uid); | ||||
|  | ||||
| 		helpers.formatApiResponse(200, res); | ||||
| 	} catch (e) { | ||||
| 		helpers.formatApiResponse(400, res, e); | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| Controller.unfollow = async (req, res) => { | ||||
| 	console.log('got here'); | ||||
| 	res.sendStatus(201); | ||||
| 	try { | ||||
| 		const { uid: objectId } = req.params; | ||||
| 		await activitypub.send(req.uid, objectId, { | ||||
| 			type: 'Unfollow', | ||||
| 			object: { | ||||
| 				type: 'Person', | ||||
| 				name: objectId, | ||||
| 			}, | ||||
| 		}); | ||||
|  | ||||
| 		await db.sortedSetRemove(`followingRemote:${req.uid}`, objectId); | ||||
| 		await recountFollowing(req.uid); | ||||
|  | ||||
| 		helpers.formatApiResponse(200, res); | ||||
| 	} catch (e) { | ||||
| 		helpers.formatApiResponse(400, res, e); | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| async function recountFollowing(uid) { | ||||
| 	const [followingCount, followingRemoteCount] = await Promise.all([ | ||||
| 		db.sortedSetCard(`following:${uid}`), | ||||
| 		db.sortedSetCard(`followingRemote:${uid}`), | ||||
| 	]); | ||||
| 	await user.setUserField(uid, 'followingCount', followingCount + followingRemoteCount); | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const { getActor } = require('../../activitypub'); | ||||
| const { getActor, outbox } = require('../../activitypub'); | ||||
|  | ||||
| const controller = module.exports; | ||||
|  | ||||
| @@ -11,6 +11,8 @@ controller.get = async function (req, res, next) { | ||||
| 		return next(); | ||||
| 	} | ||||
| 	const { preferredUsername, published, icon, image, name, summary, hostname } = actor; | ||||
| 	const isFollowing = await outbox.isFollowing(req.uid, uid); | ||||
|  | ||||
| 	const payload = { | ||||
| 		uid, | ||||
| 		username: `${preferredUsername}@${hostname}`, | ||||
| @@ -23,6 +25,8 @@ controller.get = async function (req, res, next) { | ||||
| 		'cover:position': '50% 50%', | ||||
| 		aboutme: summary, | ||||
| 		aboutmeParsed: summary, | ||||
|  | ||||
| 		isFollowing, | ||||
| 	}; | ||||
|  | ||||
| 	res.render('account/profile', payload); | ||||
|   | ||||
| @@ -49,13 +49,15 @@ module.exports = function (User) { | ||||
| 			]); | ||||
| 		} | ||||
|  | ||||
| 		const [followingCount, followerCount] = await Promise.all([ | ||||
| 		const [followingCount, followingRemoteCount, followerCount, followerRemoteCount] = await Promise.all([ | ||||
| 			db.sortedSetCard(`following:${uid}`), | ||||
| 			db.sortedSetCard(`followingRemote:${uid}`), | ||||
| 			db.sortedSetCard(`followers:${theiruid}`), | ||||
| 			db.sortedSetCard(`followersRemote:${theiruid}`), | ||||
| 		]); | ||||
| 		await Promise.all([ | ||||
| 			User.setUserField(uid, 'followingCount', followingCount), | ||||
| 			User.setUserField(theiruid, 'followerCount', followerCount), | ||||
| 			User.setUserField(uid, 'followingCount', followingCount + followingRemoteCount), | ||||
| 			User.setUserField(theiruid, 'followerCount', followerCount + followerRemoteCount), | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user