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-21 05:15:59 +00:00
|
|
|
|
2023-01-23 21:45:56 +00:00
|
|
|
// deps
|
2023-01-22 20:51:32 +00:00
|
|
|
const { isString, get, isObject } = require('lodash');
|
|
|
|
|
|
|
|
const APPublicCollectionId = 'https://www.w3.org/ns/activitystreams#Public';
|
|
|
|
const APPublicOwningUserId = 0;
|
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-21 05:15:59 +00:00
|
|
|
static followers(owningUser, page, webServer, cb) {
|
|
|
|
return Collection.getOrdered(
|
|
|
|
'followers',
|
|
|
|
owningUser,
|
|
|
|
false,
|
|
|
|
page,
|
|
|
|
e => e.id,
|
|
|
|
webServer,
|
|
|
|
cb
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static following(owningUser, page, webServer, cb) {
|
|
|
|
return Collection.getOrdered(
|
|
|
|
'following',
|
|
|
|
owningUser,
|
|
|
|
false,
|
|
|
|
page,
|
|
|
|
e => get(e, 'object.id'),
|
|
|
|
webServer,
|
|
|
|
cb
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static addFollower(owningUser, followingActor, cb) {
|
2023-01-21 08:19:19 +00:00
|
|
|
return Collection.addToCollection(
|
|
|
|
'followers',
|
|
|
|
owningUser,
|
|
|
|
followingActor.id,
|
|
|
|
followingActor,
|
2023-01-22 01:51:54 +00:00
|
|
|
false,
|
|
|
|
cb
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static outbox(owningUser, page, webServer, cb) {
|
|
|
|
return Collection.getOrdered(
|
|
|
|
'outbox',
|
|
|
|
owningUser,
|
|
|
|
false,
|
|
|
|
page,
|
|
|
|
null,
|
|
|
|
webServer,
|
|
|
|
cb
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-01-22 18:02:45 +00:00
|
|
|
static addOutboxItem(owningUser, outboxItem, isPrivate, cb) {
|
2023-01-22 01:51:54 +00:00
|
|
|
return Collection.addToCollection(
|
|
|
|
'outbox',
|
|
|
|
owningUser,
|
|
|
|
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-22 20:51:32 +00:00
|
|
|
static addPublicInboxItem(inboxItem, cb) {
|
|
|
|
return Collection.addToCollection(
|
|
|
|
'publicInbox',
|
|
|
|
APPublicOwningUserId,
|
|
|
|
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(
|
|
|
|
`SELECT obj_json
|
|
|
|
FROM collection
|
|
|
|
WHERE name = ?
|
|
|
|
${privateQuery}
|
|
|
|
AND json_extract(obj_json, '$.object.id') = ?;`,
|
|
|
|
[collectionName, objectId],
|
|
|
|
(err, row) => {
|
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!row) {
|
|
|
|
return cb(
|
|
|
|
Errors.DoesNotExist(
|
|
|
|
`No embedded Object with object.id of "${objectId}" found`
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const obj = ActivityPubObject.fromJsonString(row.obj_json);
|
|
|
|
if (!obj) {
|
|
|
|
return cb(Errors.Invalid('Failed to parse Object JSON'));
|
|
|
|
}
|
|
|
|
|
|
|
|
return cb(null, obj);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static getOrdered(
|
|
|
|
collectionName,
|
|
|
|
owningUser,
|
|
|
|
includePrivate,
|
|
|
|
page,
|
|
|
|
mapper,
|
|
|
|
webServer,
|
|
|
|
cb
|
|
|
|
) {
|
2023-01-22 01:51:54 +00:00
|
|
|
const privateQuery = includePrivate ? '' : ' AND is_private = FALSE';
|
2023-01-22 20:51:32 +00:00
|
|
|
const owningUserId = isObject(owningUser) ? owningUser.userId : owningUser;
|
2023-01-22 01:51:54 +00:00
|
|
|
|
2023-01-23 21:45:56 +00:00
|
|
|
// e.g. http://some.host/_enig/ap/collections/1234/followers
|
|
|
|
const collectionIdBase =
|
|
|
|
makeUserUrl(webServer, owningUser, `/ap/collections/${owningUserId}`) +
|
|
|
|
`/${collectionName}`;
|
|
|
|
|
2023-01-14 04:27:02 +00:00
|
|
|
if (!page) {
|
2023-01-20 23:03:27 +00:00
|
|
|
return apDb.get(
|
|
|
|
`SELECT COUNT(id) AS count
|
2023-01-21 08:19:19 +00:00
|
|
|
FROM collection
|
2023-01-22 01:51:54 +00:00
|
|
|
WHERE user_id = ? AND name = ?${privateQuery};`,
|
2023-01-23 21:45:56 +00:00
|
|
|
[owningUserId, 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-23 21:45:56 +00:00
|
|
|
id: collectionIdBase,
|
2023-01-21 05:15:59 +00:00
|
|
|
type: 'OrderedCollection',
|
2023-01-23 21:45:56 +00:00
|
|
|
first: `${collectionIdBase}?page=1`,
|
2023-01-21 05:15:59 +00:00
|
|
|
totalItems: row.count,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
obj = {
|
2023-01-23 21:45:56 +00:00
|
|
|
id: collectionIdBase,
|
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-21 08:19:19 +00:00
|
|
|
`SELECT obj_json
|
|
|
|
FROM collection
|
2023-01-22 01:51:54 +00:00
|
|
|
WHERE user_id = ? AND name = ?${privateQuery}
|
2023-01-20 23:03:27 +00:00
|
|
|
ORDER BY timestamp;`,
|
2023-01-23 21:45:56 +00:00
|
|
|
[owningUserId, 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-23 21:45:56 +00:00
|
|
|
id: `${collectionIdBase}/page=${page}`,
|
2023-01-20 23:03:27 +00:00
|
|
|
type: 'OrderedCollectionPage',
|
|
|
|
totalItems: entries.length,
|
|
|
|
orderedItems: entries,
|
2023-01-23 21:45:56 +00:00
|
|
|
partOf: collectionIdBase,
|
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-23 21:45:56 +00:00
|
|
|
static addToCollection(collectionName, owningUser, 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-22 20:51:32 +00:00
|
|
|
const owningUserId = isObject(owningUser) ? owningUser.userId : owningUser;
|
2023-01-22 01:51:54 +00:00
|
|
|
isPrivate = isPrivate ? 1 : 0;
|
2023-01-20 23:03:27 +00:00
|
|
|
apDb.run(
|
2023-01-22 01:51:54 +00:00
|
|
|
`INSERT OR IGNORE INTO collection (name, timestamp, user_id, obj_id, obj_json, is_private)
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?);`,
|
2023-01-23 21:45:56 +00:00
|
|
|
[
|
|
|
|
collectionName,
|
|
|
|
getISOTimestampString(),
|
|
|
|
owningUserId,
|
|
|
|
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) {
|
|
|
|
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-22 20:51:32 +00:00
|
|
|
const owningUserId = isObject(owningUser) ? owningUser.userId : owningUser;
|
2023-01-21 08:19:19 +00:00
|
|
|
apDb.run(
|
|
|
|
`DELETE FROM collection
|
|
|
|
WHERE user_id = ? AND name = ? AND obj_id = ?;`,
|
2023-01-23 21:45:56 +00:00
|
|
|
[owningUserId, collectionName, objectId],
|
2023-01-21 08:19:19 +00:00
|
|
|
err => {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
2023-01-14 04:27:02 +00:00
|
|
|
};
|