2023-01-20 05:31:14 +00:00
const { makeUserUrl } = require ( './util' ) ;
2023-01-20 23:03:27 +00:00
const ActivityPubObject = require ( './object' ) ;
const apDb = require ( '../database' ) . dbs . activitypub ;
const { getISOTimestampString } = require ( '../database' ) ;
2023-01-23 21:45:56 +00:00
const { Errors } = require ( '../enig_error.js' ) ;
2023-01-25 04:40:12 +00:00
const { PublicCollectionId : APPublicCollectionId } = require ( './const' ) ;
2023-01-28 18:55:31 +00:00
const UserProps = require ( '../user_property' ) ;
2023-01-21 05:15:59 +00:00
2023-01-23 21:45:56 +00:00
// deps
2023-01-28 18:55:31 +00:00
const { isString } = require ( 'lodash' ) ;
2023-01-14 04:27:02 +00:00
2023-01-20 23:03:27 +00:00
module . exports = class Collection extends ActivityPubObject {
2023-01-14 04:27:02 +00:00
constructor ( obj ) {
2023-01-20 23:03:27 +00:00
super ( obj ) ;
2023-01-14 04:27:02 +00:00
}
2023-01-22 20:51:32 +00:00
static get PublicCollectionId ( ) {
return APPublicCollectionId ;
}
2023-01-28 18:55:31 +00:00
static followers ( collectionId , page , cb ) {
return Collection . publicOrderedById (
2023-01-21 05:15:59 +00:00
'followers' ,
2023-01-28 18:55:31 +00:00
collectionId ,
2023-01-21 05:15:59 +00:00
page ,
e => e . id ,
cb
) ;
}
2023-01-28 18:55:31 +00:00
static following ( collectionId , page , cb ) {
return Collection . publicOrderedById (
2023-01-21 05:15:59 +00:00
'following' ,
2023-01-28 18:55:31 +00:00
collectionId ,
2023-01-21 05:15:59 +00:00
page ,
2023-01-28 18:55:31 +00:00
e => e . id ,
2023-01-21 05:15:59 +00:00
cb
) ;
}
2023-01-28 18:55:31 +00:00
static addFollower ( owningUser , followingActor , webServer , cb ) {
const collectionId =
makeUserUrl ( webServer , owningUser , '/ap/collections/' ) + '/followers' ;
2023-01-21 08:19:19 +00:00
return Collection . addToCollection (
'followers' ,
owningUser ,
2023-01-28 18:55:31 +00:00
collectionId ,
2023-01-21 08:19:19 +00:00
followingActor . id ,
followingActor ,
2023-01-22 01:51:54 +00:00
false ,
cb
) ;
}
2023-01-28 18:55:31 +00:00
static addFollowRequest ( owningUser , requestingActor , webServer , cb ) {
const collectionId =
makeUserUrl ( webServer , owningUser , '/ap/collections/' ) + '/follow-requests' ;
2023-01-25 01:11:28 +00:00
return Collection . addToCollection (
2023-01-28 18:55:31 +00:00
'follow-requests' ,
2023-01-25 01:11:28 +00:00
owningUser ,
2023-01-28 18:55:31 +00:00
collectionId ,
2023-01-25 01:11:28 +00:00
requestingActor . id ,
requestingActor ,
true ,
cb
) ;
}
2023-01-28 18:55:31 +00:00
static outbox ( collectionId , page , cb ) {
return Collection . publicOrderedById ( 'outbox' , collectionId , page , null , cb ) ;
2023-01-22 01:51:54 +00:00
}
2023-01-28 18:55:31 +00:00
static addOutboxItem ( owningUser , outboxItem , isPrivate , webServer , cb ) {
const collectionId =
makeUserUrl ( webServer , owningUser , '/ap/collections/' ) + '/outbox' ;
2023-01-22 01:51:54 +00:00
return Collection . addToCollection (
'outbox' ,
owningUser ,
2023-01-28 18:55:31 +00:00
collectionId ,
2023-01-22 01:51:54 +00:00
outboxItem . id ,
outboxItem ,
2023-01-22 18:02:45 +00:00
isPrivate ,
2023-01-21 08:19:19 +00:00
cb
) ;
2023-01-21 05:15:59 +00:00
}
2023-01-28 18:55:31 +00:00
static addInboxItem ( inboxItem , owningUser , webServer , cb ) {
const collectionId =
makeUserUrl ( webServer , owningUser , '/ap/collections/' ) + '/inbox' ;
2023-01-25 04:40:12 +00:00
return Collection . addToCollection (
'inbox' ,
owningUser ,
2023-01-28 18:55:31 +00:00
collectionId ,
2023-01-25 04:40:12 +00:00
inboxItem . id ,
inboxItem ,
true ,
cb
) ;
}
2023-01-22 20:51:32 +00:00
static addPublicInboxItem ( inboxItem , cb ) {
return Collection . addToCollection (
'publicInbox' ,
2023-01-28 18:55:31 +00:00
null , // N/A
Collection . PublicCollectionId ,
2023-01-22 20:51:32 +00:00
inboxItem . id ,
inboxItem ,
false ,
cb
) ;
}
2023-01-23 21:45:56 +00:00
static embeddedObjById ( collectionName , includePrivate , objectId , cb ) {
const privateQuery = includePrivate ? '' : ' AND is_private = FALSE' ;
apDb . get (
2023-01-28 18:55:31 +00:00
` SELECT object_json
2023-01-23 21:45:56 +00:00
FROM collection
WHERE name = ?
$ { privateQuery }
2023-01-28 18:55:31 +00:00
AND json _extract ( object _json , '$.object.id' ) = ? ; ` ,
2023-01-23 21:45:56 +00:00
[ collectionName , objectId ] ,
( err , row ) => {
if ( err ) {
return cb ( err ) ;
}
if ( ! row ) {
return cb (
Errors . DoesNotExist (
` No embedded Object with object.id of " ${ objectId } " found `
)
) ;
}
2023-01-28 18:55:31 +00:00
const obj = ActivityPubObject . fromJsonString ( row . object _json ) ;
2023-01-23 21:45:56 +00:00
if ( ! obj ) {
return cb ( Errors . Invalid ( 'Failed to parse Object JSON' ) ) ;
}
return cb ( null , obj ) ;
}
) ;
}
2023-01-28 18:55:31 +00:00
static publicOrderedById ( collectionName , collectionId , page , mapper , cb ) {
if ( ! page ) {
return apDb . get (
` SELECT COUNT(collection_id) AS count
FROM collection
WHERE name = ? AND collection _id = ? AND is _private = FALSE ; ` ,
[ collectionName , collectionId ] ,
( err , row ) => {
if ( err ) {
return cb ( err ) ;
}
let obj ;
if ( row . count > 0 ) {
obj = {
id : collectionId ,
type : 'OrderedCollection' ,
first : ` ${ collectionId } ?page=1 ` ,
totalItems : row . count ,
} ;
} else {
obj = {
id : collectionId ,
type : 'OrderedCollection' ,
totalItems : 0 ,
orderedItems : [ ] ,
} ;
}
return cb ( null , new Collection ( obj ) ) ;
}
) ;
}
// :TODO: actual paging...
apDb . all (
` SELECT object_json
FROM collection
WHERE name = ? AND collection _id = ? AND is _private = FALSE
ORDER BY timestamp ; ` ,
[ collectionName , collectionId ] ,
( err , entries ) => {
if ( err ) {
return cb ( err ) ;
}
entries = entries || [ ] ;
if ( mapper && entries . length > 0 ) {
entries = entries . map ( mapper ) ;
}
const obj = {
id : ` ${ collectionId } /page= ${ page } ` ,
type : 'OrderedCollectionPage' ,
totalItems : entries . length ,
orderedItems : entries ,
partOf : collectionId ,
} ;
return cb ( null , new Collection ( obj ) ) ;
}
) ;
}
static ownedOrderedByUser (
2023-01-23 21:45:56 +00:00
collectionName ,
owningUser ,
includePrivate ,
page ,
mapper ,
webServer ,
cb
) {
2023-01-22 01:51:54 +00:00
const privateQuery = includePrivate ? '' : ' AND is_private = FALSE' ;
2023-01-28 18:55:31 +00:00
const actorId = owningUser . getProperty ( UserProps . ActivityPubActorId ) ;
if ( ! actorId ) {
return cb (
Errors . MissingProperty (
` User " ${ owningUser . username } " is missing property ' ${ UserProps . ActivityPubActorId } ' `
)
) ;
}
2023-01-22 01:51:54 +00:00
2023-01-28 18:55:31 +00:00
// e.g. http://somewhere.com/_enig/ap/collections/NuSkooler/followers
const collectionId =
makeUserUrl ( webServer , owningUser , '/ap/collections/' ) + ` / ${ collectionName } ` ;
2023-01-23 21:45:56 +00:00
2023-01-14 04:27:02 +00:00
if ( ! page ) {
2023-01-20 23:03:27 +00:00
return apDb . get (
2023-01-28 18:55:31 +00:00
` SELECT COUNT(collection_id) AS count
2023-01-21 08:19:19 +00:00
FROM collection
2023-01-28 18:55:31 +00:00
WHERE owner _actor _id = ? AND name = ? $ { privateQuery } ; ` ,
[ actorId , collectionName ] ,
2023-01-20 23:03:27 +00:00
( err , row ) => {
if ( err ) {
return cb ( err ) ;
}
2023-01-14 04:27:02 +00:00
2023-01-22 01:51:54 +00:00
//
// Mastodon for instance, will never follow up for the
// actual data from some Collections such as 'followers';
// Instead, they only use the |totalItems| to form an
// approximate follower count.
//
2023-01-21 05:15:59 +00:00
let obj ;
if ( row . count > 0 ) {
obj = {
2023-01-28 18:55:31 +00:00
id : collectionId ,
2023-01-21 05:15:59 +00:00
type : 'OrderedCollection' ,
2023-01-28 18:55:31 +00:00
first : ` ${ collectionId } ?page=1 ` ,
2023-01-21 05:15:59 +00:00
totalItems : row . count ,
} ;
} else {
obj = {
2023-01-28 18:55:31 +00:00
id : collectionId ,
2023-01-21 05:15:59 +00:00
type : 'OrderedCollection' ,
totalItems : 0 ,
orderedItems : [ ] ,
} ;
}
2023-01-14 04:27:02 +00:00
2023-01-20 23:03:27 +00:00
return cb ( null , new Collection ( obj ) ) ;
}
) ;
2023-01-14 04:27:02 +00:00
}
2023-01-20 23:03:27 +00:00
// :TODO: actual paging...
apDb . all (
2023-01-28 18:55:31 +00:00
` SELECT object_json
2023-01-21 08:19:19 +00:00
FROM collection
2023-01-28 18:55:31 +00:00
WHERE owner _actor _id = ? AND name = ? $ { privateQuery }
2023-01-20 23:03:27 +00:00
ORDER BY timestamp ; ` ,
2023-01-28 18:55:31 +00:00
[ actorId , collectionName ] ,
2023-01-20 23:03:27 +00:00
( err , entries ) => {
if ( err ) {
return cb ( err ) ;
}
2023-01-21 05:15:59 +00:00
entries = entries || [ ] ;
if ( mapper && entries . length > 0 ) {
2023-01-20 23:03:27 +00:00
entries = entries . map ( mapper ) ;
}
const obj = {
2023-01-28 18:55:31 +00:00
id : ` ${ collectionId } /page= ${ page } ` ,
2023-01-20 23:03:27 +00:00
type : 'OrderedCollectionPage' ,
totalItems : entries . length ,
orderedItems : entries ,
2023-01-28 18:55:31 +00:00
partOf : collectionId ,
2023-01-20 23:03:27 +00:00
} ;
return cb ( null , new Collection ( obj ) ) ;
2023-01-14 04:27:02 +00:00
}
2023-01-20 23:03:27 +00:00
) ;
}
2023-01-14 04:27:02 +00:00
2023-01-25 01:11:28 +00:00
// https://www.w3.org/TR/activitypub/#update-activity-inbox
static updateCollectionEntry ( collectionName , objectId , obj , cb ) {
if ( ! isString ( obj ) ) {
obj = JSON . stringify ( obj ) ;
}
// :TODO: The receiving server MUST take care to be sure that the Update is authorized to modify its object. At minimum, this may be done by ensuring that the Update and its object are of same origin.
apDb . run (
` UPDATE collection
2023-01-28 18:55:31 +00:00
SET object _json = ? , timestamp = ?
WHERE name = ? AND object _id = ? ; ` ,
2023-01-25 01:11:28 +00:00
[ obj , collectionName , getISOTimestampString ( ) , objectId ] ,
err => {
return cb ( err ) ;
}
) ;
}
2023-01-28 18:55:31 +00:00
static addToCollection (
collectionName ,
owningUser ,
collectionId ,
objectId ,
obj ,
isPrivate ,
cb
) {
2023-01-21 08:19:19 +00:00
if ( ! isString ( obj ) ) {
obj = JSON . stringify ( obj ) ;
2023-01-20 23:03:27 +00:00
}
2023-01-14 04:27:02 +00:00
2023-01-28 18:55:31 +00:00
let actorId ;
if ( owningUser ) {
actorId = owningUser . getProperty ( UserProps . ActivityPubActorId ) ;
if ( ! actorId ) {
return cb (
Errors . MissingProperty (
` User " ${ owningUser . username } " is missing property ' ${ UserProps . ActivityPubActorId } ' `
)
) ;
}
} else {
actorId = Collection . APPublicCollectionId ;
}
2023-01-22 01:51:54 +00:00
isPrivate = isPrivate ? 1 : 0 ;
2023-01-28 18:55:31 +00:00
2023-01-20 23:03:27 +00:00
apDb . run (
2023-01-28 18:55:31 +00:00
` INSERT OR IGNORE INTO collection (name, timestamp, collection_id, owner_actor_id, object_id, object_json, is_private)
VALUES ( ? , ? , ? , ? , ? , ? , ? ) ; ` ,
2023-01-23 21:45:56 +00:00
[
collectionName ,
getISOTimestampString ( ) ,
2023-01-28 18:55:31 +00:00
collectionId ,
actorId ,
2023-01-23 21:45:56 +00:00
objectId ,
obj ,
isPrivate ,
] ,
2023-01-20 23:03:27 +00:00
function res ( err ) {
// non-arrow for 'this' scope
2023-01-21 05:15:59 +00:00
if ( err ) {
2023-01-28 18:55:31 +00:00
if ( 'SQLITE_CONSTRAINT' === err . code ) {
err = null ; // ignore
}
2023-01-21 05:15:59 +00:00
return cb ( err ) ;
}
2023-01-20 23:03:27 +00:00
return cb ( err , this . lastID ) ;
}
) ;
}
2023-01-21 08:19:19 +00:00
2023-01-23 21:45:56 +00:00
static removeFromCollectionById ( collectionName , owningUser , objectId , cb ) {
2023-01-28 18:55:31 +00:00
const actorId = owningUser . getProperty ( UserProps . ActivityPubActorId ) ;
if ( ! actorId ) {
return cb (
Errors . MissingProperty (
` User " ${ owningUser . username } " is missing property ' ${ UserProps . ActivityPubActorId } ' `
)
) ;
}
2023-01-21 08:19:19 +00:00
apDb . run (
` DELETE FROM collection
2023-01-28 18:55:31 +00:00
WHERE name = ? AND owner _actor _id = ? AND object _id = ? ; ` ,
[ collectionName , actorId , objectId ] ,
2023-01-21 08:19:19 +00:00
err => {
return cb ( err ) ;
}
) ;
}
2023-01-14 04:27:02 +00:00
} ;