2024-01-26 15:10:35 -05:00
'use strict' ;
2024-04-05 11:37:23 -04:00
const nconf = require ( 'nconf' ) ;
2024-06-07 16:27:44 -04:00
const winston = require ( 'winston' ) ;
2025-02-11 13:45:38 -05:00
const _ = require ( 'lodash' ) ;
2024-01-26 15:10:35 -05:00
const db = require ( '../database' ) ;
2024-06-07 16:27:44 -04:00
const meta = require ( '../meta' ) ;
const batch = require ( '../batch' ) ;
2024-03-05 09:56:15 -05:00
const user = require ( '../user' ) ;
2024-01-26 15:10:35 -05:00
const utils = require ( '../utils' ) ;
2024-03-21 00:25:27 +01:00
const TTLCache = require ( '../cache/ttl' ) ;
2024-06-04 12:31:13 -04:00
const failedWebfingerCache = TTLCache ( {
max : 5000 ,
ttl : 1000 * 60 * 10 , // 10 minutes
} ) ;
2024-01-26 15:10:35 -05:00
const activitypub = module . parent . exports ;
const Actors = module . exports ;
2024-01-26 16:48:16 -05:00
Actors . assert = async ( ids , options = { } ) => {
2024-09-19 14:52:05 -04:00
/ * *
* Ensures that the passed in ids or webfinger handles are stored in database .
* Options :
* - update : boolean , forces re - fetch / process of the resolved id
* Return one of :
* - An array of newly processed ids
* - false : if input incorrect ( or webfinger handle cannot resolve )
* - true : no new IDs processed ; all passed - in IDs present .
* /
2024-01-26 15:10:35 -05:00
// Handle single values
if ( ! Array . isArray ( ids ) ) {
ids = [ ids ] ;
}
2024-06-07 12:13:28 -04:00
if ( ! ids . length ) {
return false ;
}
2024-04-15 13:37:00 -04:00
// Existance in failure cache is automatic assertion failure
if ( ids . some ( id => failedWebfingerCache . has ( id ) ) ) {
return false ;
}
2024-01-26 15:10:35 -05:00
// Filter out uids if passed in
2024-04-15 13:37:00 -04:00
ids = ids . filter ( id => ! utils . isNumber ( id ) ) ;
2024-01-26 15:10:35 -05:00
2024-03-07 15:39:42 -05:00
// Translate webfinger handles to uris
2024-08-02 11:47:23 -04:00
const hostMap = new Map ( ) ;
2024-04-05 11:37:23 -04:00
ids = ( await Promise . all ( ids . map ( async ( id ) => {
2024-03-21 00:25:27 +01:00
const originalId = id ;
2024-05-07 10:11:36 -04:00
if ( activitypub . helpers . isWebfinger ( id ) ) {
2024-07-29 15:03:51 -04:00
const host = id . replace ( /^(acct:|@)/ , '' ) . split ( '@' ) [ 1 ] ;
2024-04-05 11:37:23 -04:00
if ( host === nconf . get ( 'url_parsed' ) . host ) { // do not assert loopback ids
2024-04-26 11:30:08 -04:00
return 'loopback' ;
2024-04-05 11:37:23 -04:00
}
2024-03-07 15:39:42 -05:00
( { actorUri : id } = await activitypub . helpers . query ( id ) ) ;
2024-08-02 11:47:23 -04:00
hostMap . set ( id , host ) ;
2024-03-07 15:39:42 -05:00
}
2024-05-06 23:57:47 +02:00
// ensure the final id is a valid URI
if ( ! id || ! activitypub . helpers . isUri ( id ) ) {
2024-03-21 00:25:27 +01:00
failedWebfingerCache . set ( originalId , true ) ;
2024-05-06 23:57:47 +02:00
return ;
2024-03-21 00:25:27 +01:00
}
2024-03-07 15:39:42 -05:00
return id ;
2024-04-15 13:37:00 -04:00
} ) ) ) ;
// Webfinger failures = assertion failure
2025-01-24 23:41:19 -05:00
if ( ! ids . length || ! ids . every ( Boolean ) ) {
2024-04-15 13:37:00 -04:00
return false ;
}
2024-03-07 15:39:42 -05:00
2024-04-05 11:37:23 -04:00
// Filter out loopback uris
2024-04-26 11:30:08 -04:00
ids = ids . filter ( uri => uri !== 'loopback' && new URL ( uri ) . host !== nconf . get ( 'url_parsed' ) . host ) ;
2024-03-07 15:39:42 -05:00
2024-06-14 10:43:03 -04:00
// Only assert those who haven't been seen recently (configurable), unless update flag passed in (force refresh)
2024-01-26 16:48:16 -05:00
if ( ! options . update ) {
2024-06-14 10:43:03 -04:00
const upperBound = Date . now ( ) - ( 1000 * 60 * 60 * 24 * meta . config . activitypubUserPruneDays ) ;
const lastCrawled = await db . sortedSetScores ( 'usersRemote:lastCrawled' , ids . map ( id => ( ( typeof id === 'object' && id . hasOwnProperty ( 'id' ) ) ? id . id : id ) ) ) ;
ids = ids . filter ( ( id , idx ) => {
const timestamp = lastCrawled [ idx ] ;
return ! timestamp || timestamp < upperBound ;
} ) ;
2024-01-26 16:48:16 -05:00
}
2024-01-26 15:10:35 -05:00
if ( ! ids . length ) {
return true ;
}
2024-10-12 22:49:24 -04:00
activitypub . helpers . log ( ` [activitypub/actors] Asserting ${ ids . length } actor(s) ` ) ;
2024-02-20 11:57:50 -05:00
2024-06-07 12:55:52 -04:00
// NOTE: MAKE SURE EVERY DB ADDITION HAS A CORRESPONDING REMOVAL IN ACTORS.REMOVE!
2024-05-24 14:11:06 -04:00
const urlMap = new Map ( ) ;
2024-02-12 14:32:55 -05:00
const followersUrlMap = new Map ( ) ;
2024-02-21 13:43:56 -05:00
const pubKeysMap = new Map ( ) ;
2024-03-07 16:59:40 -05:00
let actors = await Promise . all ( ids . map ( async ( id ) => {
2024-01-26 15:10:35 -05:00
try {
2024-10-12 22:49:24 -04:00
activitypub . helpers . log ( ` [activitypub/actors] Processing ${ id } ` ) ;
2024-07-10 13:47:23 -04:00
const actor = ( typeof id === 'object' && id . hasOwnProperty ( 'id' ) ) ? id : await activitypub . get ( 'uid' , 0 , id , { cache : process . env . CI === 'true' } ) ;
2024-09-19 14:52:05 -04:00
if (
! activitypub . _constants . acceptableActorTypes . has ( actor . type ) ||
! activitypub . _constants . requiredActorProps . every ( prop => actor . hasOwnProperty ( prop ) )
) {
return null ;
}
2024-01-26 15:10:35 -05:00
// Follow counts
2024-01-26 16:24:14 -05:00
try {
const [ followers , following ] = await Promise . all ( [
2024-02-05 16:57:17 -05:00
actor . followers ? activitypub . get ( 'uid' , 0 , actor . followers ) : { totalItems : 0 } ,
actor . following ? activitypub . get ( 'uid' , 0 , actor . following ) : { totalItems : 0 } ,
2024-01-26 16:24:14 -05:00
] ) ;
actor . followerCount = followers . totalItems ;
actor . followingCount = following . totalItems ;
} catch ( e ) {
// no action required
2024-10-12 22:49:24 -04:00
activitypub . helpers . log ( ` [activitypub/actor.assert] Unable to retrieve follower counts for ${ actor . id } ` ) ;
2024-01-26 16:24:14 -05:00
}
2024-01-26 15:10:35 -05:00
2024-05-24 14:11:06 -04:00
// Save url for backreference
const url = Array . isArray ( actor . url ) ? actor . url . shift ( ) : actor . url ;
if ( url && url !== actor . id ) {
urlMap . set ( url , actor . id ) ;
}
// Save followers url for backreference
2024-02-12 14:32:55 -05:00
if ( actor . hasOwnProperty ( 'followers' ) && activitypub . helpers . isUri ( actor . followers ) ) {
followersUrlMap . set ( actor . followers , actor . id ) ;
}
2024-02-21 13:43:56 -05:00
// Public keys
pubKeysMap . set ( actor . id , actor . publicKey ) ;
2024-01-26 15:10:35 -05:00
return actor ;
} catch ( e ) {
2024-06-13 14:53:47 -04:00
if ( e . code === 'ap_get_410' ) {
const exists = await user . exists ( id ) ;
if ( exists ) {
await user . deleteAccount ( id ) ;
}
}
2024-01-26 15:10:35 -05:00
return null ;
}
} ) ) ;
2024-03-07 16:59:40 -05:00
actors = actors . filter ( Boolean ) ; // remove unresolvable actors
2024-01-26 15:10:35 -05:00
// Build userData object for storage
2024-08-02 11:47:23 -04:00
const profiles = ( await activitypub . mocks . profile ( actors , hostMap ) ) . filter ( Boolean ) ;
2024-01-26 15:10:35 -05:00
const now = Date . now ( ) ;
2024-02-21 13:43:56 -05:00
const bulkSet = profiles . reduce ( ( memo , profile ) => {
2024-03-07 16:59:40 -05:00
const key = ` userRemote: ${ profile . uid } ` ;
memo . push ( [ key , profile ] , [ ` ${ key } :keys ` , pubKeysMap . get ( profile . uid ) ] ) ;
2024-02-21 13:43:56 -05:00
return memo ;
} , [ ] ) ;
2024-05-24 14:11:06 -04:00
if ( urlMap . size ) {
bulkSet . push ( [ 'remoteUrl:uid' , Object . fromEntries ( urlMap ) ] ) ;
}
2024-02-12 14:32:55 -05:00
if ( followersUrlMap . size ) {
bulkSet . push ( [ 'followersUrl:uid' , Object . fromEntries ( followersUrlMap ) ] ) ;
}
2024-03-05 09:56:15 -05:00
const exists = await db . isSortedSetMembers ( 'usersRemote:lastCrawled' , profiles . map ( p => p . uid ) ) ;
const uidsForCurrent = profiles . map ( ( p , idx ) => ( exists [ idx ] ? p . uid : 0 ) ) ;
2024-06-14 11:05:27 -04:00
const current = await user . getUsersFields ( uidsForCurrent , [ 'username' , 'fullname' ] ) ;
2024-03-05 14:24:13 -05:00
const queries = profiles . reduce ( ( memo , profile , idx ) => {
2024-03-07 16:59:40 -05:00
const { username , fullname } = current [ idx ] ;
2024-03-05 14:24:13 -05:00
2024-12-11 12:53:09 -05:00
if ( options . update || username !== profile . username ) {
2024-03-07 16:59:40 -05:00
if ( uidsForCurrent [ idx ] !== 0 ) {
memo . searchRemove . push ( [ 'ap.preferredUsername:sorted' , ` ${ username . toLowerCase ( ) } : ${ profile . uid } ` ] ) ;
memo . handleRemove . push ( username . toLowerCase ( ) ) ;
2024-03-05 09:56:15 -05:00
}
2024-03-07 16:59:40 -05:00
memo . searchAdd . push ( [ 'ap.preferredUsername:sorted' , 0 , ` ${ profile . username . toLowerCase ( ) } : ${ profile . uid } ` ] ) ;
memo . handleAdd [ profile . username . toLowerCase ( ) ] = profile . uid ;
}
2024-03-05 14:24:13 -05:00
2024-12-22 13:34:29 -05:00
if ( options . update || ( profile . fullname && fullname !== profile . fullname ) ) {
2024-07-05 11:18:20 -04:00
if ( fullname && uidsForCurrent [ idx ] !== 0 ) {
2024-03-07 16:59:40 -05:00
memo . searchRemove . push ( [ 'ap.name:sorted' , ` ${ fullname . toLowerCase ( ) } : ${ profile . uid } ` ] ) ;
2024-03-05 09:56:15 -05:00
}
2024-03-07 16:59:40 -05:00
memo . searchAdd . push ( [ 'ap.name:sorted' , 0 , ` ${ profile . fullname . toLowerCase ( ) } : ${ profile . uid } ` ] ) ;
2024-03-05 09:56:15 -05:00
}
return memo ;
2024-03-05 14:24:13 -05:00
} , { searchRemove : [ ] , searchAdd : [ ] , handleRemove : [ ] , handleAdd : { } } ) ;
2024-03-05 09:56:15 -05:00
2024-12-11 12:48:50 -05:00
// Removals
await Promise . all ( [
db . sortedSetRemoveBulk ( queries . searchRemove ) ,
db . deleteObjectFields ( 'handle:uid' , queries . handleRemove ) ,
] ) ;
// Additions
2024-01-26 15:10:35 -05:00
await Promise . all ( [
2024-02-12 14:32:55 -05:00
db . setObjectBulk ( bulkSet ) ,
2024-03-06 14:59:49 -05:00
db . sortedSetAdd ( 'usersRemote:lastCrawled' , profiles . map ( ( ) => now ) , profiles . map ( p => p . uid ) ) ,
2024-03-05 14:24:13 -05:00
db . sortedSetAddBulk ( queries . searchAdd ) ,
db . setObject ( 'handle:uid' , queries . handleAdd ) ,
2024-01-26 15:10:35 -05:00
] ) ;
2024-03-07 16:59:40 -05:00
return actors ;
2024-01-26 15:10:35 -05:00
} ;
2024-04-16 14:00:01 -04:00
Actors . getLocalFollowers = async ( id ) => {
const response = {
uids : new Set ( ) ,
cids : new Set ( ) ,
} ;
if ( ! activitypub . helpers . isUri ( id ) ) {
return response ;
}
const members = await db . getSortedSetMembers ( ` followersRemote: ${ id } ` ) ;
members . forEach ( ( id ) => {
if ( utils . isNumber ( id ) ) {
response . uids . add ( parseInt ( id , 10 ) ) ;
} else if ( id . startsWith ( 'cid|' ) && utils . isNumber ( id . slice ( 4 ) ) ) {
response . cids . add ( parseInt ( id . slice ( 4 ) , 10 ) ) ;
}
} ) ;
return response ;
} ;
2025-02-11 10:48:42 -05:00
Actors . getLocalFollowCounts = async ( actors ) => {
const isArray = Array . isArray ( actors ) ;
if ( ! isArray ) {
actors = [ actors ] ;
2024-04-16 14:00:01 -04:00
}
2025-02-11 10:48:42 -05:00
const validActors = actors . filter ( actor => activitypub . helpers . isUri ( actor ) ) ;
const followerKeys = validActors . map ( actor => ` followersRemote: ${ actor } ` ) ;
const followingKeys = validActors . map ( actor => ` followingRemote: ${ actor } ` ) ;
const [ followersCounts , followingCounts ] = await Promise . all ( [
db . sortedSetsCard ( followerKeys ) ,
db . sortedSetsCard ( followingKeys ) ,
2024-07-19 14:37:32 -04:00
] ) ;
2025-02-11 13:45:38 -05:00
const actorToCounts = _ . zipObject ( validActors , validActors . map (
( a , idx ) => ( { followers : followersCounts [ idx ] , following : followingCounts [ idx ] } )
) ) ;
const results = actors . map ( ( actor ) => {
if ( ! actorToCounts . hasOwnProperty ( actor ) ) {
2025-02-11 10:48:42 -05:00
return { followers : 0 , following : 0 } ;
}
2025-02-11 13:45:38 -05:00
return {
followers : actorToCounts [ actor ] . followers ,
following : actorToCounts [ actor ] . following ,
} ;
2025-02-11 10:48:42 -05:00
} ) ;
return isArray ? results : results [ 0 ] ;
2024-04-16 14:00:01 -04:00
} ;
2024-06-07 12:55:52 -04:00
Actors . remove = async ( id ) => {
2024-06-07 16:27:44 -04:00
/ * *
* Remove ActivityPub related metadata pertaining to a remote id
*
* Note : don ' t call this directly ! It is called as part of user . deleteAccount
* /
2024-06-07 12:55:52 -04:00
const exists = await db . isSortedSetMember ( 'usersRemote:lastCrawled' , id ) ;
if ( ! exists ) {
return false ;
}
let { username , fullname , url , followersUrl } = await user . getUserFields ( id , [ 'username' , 'fullname' , 'url' , 'followersUrl' ] ) ;
username = username . toLowerCase ( ) ;
2024-06-10 15:18:32 -04:00
const bulkRemove = [
[ 'ap.preferredUsername:sorted' , ` ${ username } : ${ id } ` ] ,
] ;
if ( fullname ) {
bulkRemove . push ( [ 'ap.name:sorted' , ` ${ fullname . toLowerCase ( ) } : ${ id } ` ] ) ;
}
2024-06-07 12:55:52 -04:00
await Promise . all ( [
2024-06-10 15:18:32 -04:00
db . sortedSetRemoveBulk ( bulkRemove ) ,
2024-06-07 12:55:52 -04:00
db . deleteObjectField ( 'handle:uid' , username ) ,
db . deleteObjectField ( 'followersUrl:uid' , followersUrl ) ,
db . deleteObjectField ( 'remoteUrl:uid' , url ) ,
db . delete ( ` userRemote: ${ id } :keys ` ) ,
] ) ;
await Promise . all ( [
db . delete ( ` userRemote: ${ id } ` ) ,
db . sortedSetRemove ( 'usersRemote:lastCrawled' , id ) ,
] ) ;
} ;
2024-06-07 16:27:44 -04:00
Actors . prune = async ( ) => {
/ * *
* Clear out remote user accounts that do not have content on the forum anywhere
* /
2024-06-10 15:18:32 -04:00
winston . info ( '[actors/prune] Started scheduled pruning of remote user accounts' ) ;
2024-06-07 16:27:44 -04:00
const days = parseInt ( meta . config . activitypubUserPruneDays , 10 ) ;
const timestamp = Date . now ( ) - ( 1000 * 60 * 60 * 24 * days ) ;
2025-02-11 10:39:24 -05:00
const uids = await db . getSortedSetRangeByScore ( 'usersRemote:lastCrawled' , 0 , 500 , '-inf' , timestamp ) ;
2024-06-07 16:27:44 -04:00
if ( ! uids . length ) {
2024-06-10 15:18:32 -04:00
winston . info ( '[actors/prune] No remote users to prune, all done.' ) ;
2024-06-07 16:27:44 -04:00
return ;
}
2024-06-10 15:18:32 -04:00
winston . info ( ` [actors/prune] Found ${ uids . length } remote users last crawled more than ${ days } days ago ` ) ;
2024-06-07 16:27:44 -04:00
let deletionCount = 0 ;
2025-02-11 13:45:38 -05:00
let deletionCountNonExisting = 0 ;
2025-02-11 13:48:53 -05:00
let notDeletedDueToLocalContent = 0 ;
2025-02-11 14:32:54 -05:00
const notDeletedUids = [ ] ;
2024-06-07 16:27:44 -04:00
await batch . processArray ( uids , async ( uids ) => {
const exists = await db . exists ( uids . map ( uid => ` userRemote: ${ uid } ` ) ) ;
2025-02-11 10:39:24 -05:00
const uidsThatExist = uids . filter ( ( uid , idx ) => exists [ idx ] ) ;
const uidsThatDontExist = uids . filter ( ( uid , idx ) => ! exists [ idx ] ) ;
const [ postCounts , roomCounts , followCounts ] = await Promise . all ( [
db . sortedSetsCard ( uidsThatExist . map ( uid => ` uid: ${ uid } :posts ` ) ) ,
db . sortedSetsCard ( uidsThatExist . map ( uid => ` uid: ${ uid } :chat:rooms ` ) ) ,
Actors . getLocalFollowCounts ( uidsThatExist ) ,
2024-10-10 14:49:40 -04:00
] ) ;
2024-06-07 16:27:44 -04:00
2025-02-11 10:39:24 -05:00
await Promise . all ( uidsThatExist . map ( async ( uid , idx ) => {
const { followers , following } = followCounts [ idx ] ;
2024-07-19 14:37:32 -04:00
const postCount = postCounts [ idx ] ;
2024-10-10 14:49:40 -04:00
const roomCount = roomCounts [ idx ] ;
if ( [ postCount , roomCount , followers , following ] . every ( metric => metric < 1 ) ) {
2024-06-10 19:24:06 -04:00
try {
await user . deleteAccount ( uid ) ;
deletionCount += 1 ;
} catch ( err ) {
winston . error ( err . stack ) ;
}
2025-02-11 13:48:53 -05:00
} else {
notDeletedDueToLocalContent += 1 ;
2025-02-11 14:32:54 -05:00
notDeletedUids . push ( uid ) ;
2024-06-07 16:27:44 -04:00
}
} ) ) ;
2025-02-11 13:45:38 -05:00
deletionCountNonExisting += uidsThatDontExist . length ;
await db . sortedSetRemove ( 'usersRemote:lastCrawled' , uidsThatDontExist ) ;
2025-02-11 14:32:54 -05:00
// update timestamp in usersRemote:lastCrawled so we don't try to delete users
// with content over and over
const now = Date . now ( ) ;
await db . sortedSetAdd ( 'usersRemote:lastCrawled' , notDeletedUids . map ( ( ) => now ) , notDeletedUids ) ;
2024-06-07 16:27:44 -04:00
} , {
batch : 50 ,
interval : 1000 ,
} ) ;
2025-02-11 13:48:53 -05:00
winston . info ( ` [actors/prune] ${ deletionCount } remote users pruned. ${ deletionCountNonExisting } does not exist. ${ notDeletedDueToLocalContent } not deleted due to local content ` ) ;
2024-06-07 16:27:44 -04:00
} ;