This commit is contained in:
Bryan Ashby 2023-01-08 01:29:37 -07:00
commit f9f9208ada
No known key found for this signature in database
GPG Key ID: C2C1B501E4EFD994
4 changed files with 452 additions and 20 deletions

View File

@ -16,7 +16,8 @@
"quotes": ["error", "single"],
"semi": ["error", "always"],
"comma-dangle": 0,
"no-trailing-spaces": "error"
"no-trailing-spaces": "error",
"no-control-regex": 0
},
"parserOptions": {
"ecmaVersion": 2020

383
core/activitypub_actor.js Normal file
View File

@ -0,0 +1,383 @@
/* jslint node: true */
'use strict';
// ENiGMA½
const actorDb = require('./database.js').dbs.actor;
const { Errors } = require('./enig_error.js');
const Events = require('./events.js');
const ActorProps = require('./activitypub_actor_property.js');
// deps
const assert = require('assert');
const async = require('async');
const _ = require('lodash');
module.exports = class Actor {
constructor() {
this.actorId = 0;
this.actorUrl = '';
this.properties = {}; // name:value
this.groups = []; // group membership(s)
}
create(cb) {
assert(0 === this.actorId);
if (
_.isEmpty(this.actorUrl)
) {
return cb(Errors.Invalid('Blank actor url'));
}
const self = this;
async.waterfall(
[
function beginTransaction(callback) {
return actorDb.beginTransaction(callback);
},
function createActorRec(trans, callback) {
trans.run(
`INSERT INTO actor (actor_url)
VALUES (?);`,
[self.actorUrl],
function inserted(err) {
// use classic function for |this|
if (err) {
return callback(err);
}
self.actorId = this.lastID;
return callback(null, trans);
}
);
},
function saveAll(trans, callback) {
self.persistWithTransaction(trans, err => {
return callback(err, trans);
});
},
function sendEvent(trans, callback) {
Events.emit(Events.getSystemEvents().NewActor, {
actor: Object.assign({}, self, {}),
});
return callback(null, trans);
},
],
(err, trans) => {
if (trans) {
trans[err ? 'rollback' : 'commit'](transErr => {
return cb(err ? err : transErr);
});
} else {
return cb(err);
}
}
);
}
persistWithTransaction(trans, cb) {
assert(this.actorId > 0);
const self = this;
async.series(
[
function saveProps(callback) {
self.persistProperties(self.properties, trans, err => {
return callback(err);
});
},
],
err => {
return cb(err);
}
);
}
static persistPropertyByActorId(actorId, propName, propValue, cb) {
actorDb.run(
`REPLACE INTO activitypub_actor_property (actor_id, prop_name, prop_value)
VALUES (?, ?, ?);`,
[actorId, propName, propValue],
err => {
if (cb) {
return cb(err, propValue);
}
}
);
}
setProperty(propName, propValue) {
this.properties[propName] = propValue;
}
incrementProperty(propName, incrementBy) {
incrementBy = incrementBy || 1;
let newValue = parseInt(this.getProperty(propName));
if (newValue) {
newValue += incrementBy;
} else {
newValue = incrementBy;
}
this.setProperty(propName, newValue);
return newValue;
}
getProperty(propName) {
return this.properties[propName];
}
getPropertyAsNumber(propName) {
return parseInt(this.getProperty(propName), 10);
}
persistProperty(propName, propValue, cb) {
// update live props
this.properties[propName] = propValue;
return Actor.persistPropertyByActorId(this.actorId, propName, propValue, cb);
}
removeProperty(propName, cb) {
// update live
delete this.properties[propName];
actorDb.run(
`DELETE FROM activitypub_actor_property
WHERE activity_id = ? AND prop_name = ?;`,
[this.actorId, propName],
err => {
if (cb) {
return cb(err);
}
}
);
}
removeProperties(propNames, cb) {
async.each(
propNames,
(name, next) => {
return this.removeProperty(name, next);
},
err => {
if (cb) {
return cb(err);
}
}
);
}
persistProperties(properties, transOrDb, cb) {
if (!_.isFunction(cb) && _.isFunction(transOrDb)) {
cb = transOrDb;
transOrDb = actorDb;
}
const self = this;
// update live props
_.merge(this.properties, properties);
const stmt = transOrDb.prepare(
`REPLACE INTO activitypub_actor_property (actor_id, prop_name, prop_value)
VALUES (?, ?, ?);`
);
async.each(
Object.keys(properties),
(propName, nextProp) => {
stmt.run(self.actorId, propName, properties[propName], err => {
return nextProp(err);
});
},
err => {
if (err) {
return cb(err);
}
stmt.finalize(() => {
return cb(null);
});
}
);
}
static getActor(actorId, cb) {
async.waterfall(
[
function fetchActorId(callback) {
Actor.getActorUrl(actorId, (err, actorUrl) => {
return callback(null, actorUrl);
});
},
function initProps(actorUrl, callback) {
Actor.loadProperties(actorId, (err, properties) => {
return callback(err, actorUrl, properties);
});
},
],
(err, actorUrl, properties) => {
const actor = new Actor();
actor.actorId = actorId;
actor.actorUrl = actorUrl;
actor.properties = properties;
return cb(err, actor);
}
);
}
// FIXME
static getActorInfo(actorId, propsList, cb) {
if (!cb && _.isFunction(propsList)) {
cb = propsList;
propsList = [
ActorProps.Type,
ActorProps.PreferredUsername,
ActorProps.Name,
ActorProps.Summary,
ActorProps.IconUrl,
ActorProps.BannerUrl,
ActorProps.PublicKeyMain
];
}
async.waterfall(
[
callback => {
return Actor.getActorUrl(actorId, callback);
},
(actorUrl, callback) => {
Actor.loadProperties(actorId, { names: propsList }, (err, props) => {
return callback(
err,
Object.assign({}, props, { actor_url: actorUrl })
);
});
},
],
(err, actorProps) => {
if (err) {
return cb(err);
}
const actorInfo = {};
Object.keys(actorProps).forEach(key => {
actorInfo[_.camelCase(key)] = actorProps[key] || 'N/A';
});
return cb(null, actorInfo);
}
);
}
static getActorIdAndUrl(actorUrl, cb) {
actorDb.get(
`SELECT id, actor_url
FROM activitypub_actor
WHERE actor_url LIKE ?;`,
[actorUrl],
(err, row) => {
if (err) {
return cb(err);
}
if (row) {
return cb(null, row.id, row.actor_url);
}
return cb(Errors.DoesNotExist('No matching actorUrl'));
}
);
}
static getActorUrl(actorId, cb) {
actorDb.get(
`SELECT actor_url
FROM activitypub_actor
WHERE id = ?;`,
[actorId],
(err, row) => {
if (err) {
return cb(err);
}
if (row) {
return cb(null, row.actor_url);
}
return cb(Errors.DoesNotExist('No matching actor ID'));
}
);
}
static loadProperties(actorId, options, cb) {
if (!cb && _.isFunction(options)) {
cb = options;
options = {};
}
let sql = `SELECT prop_name, prop_value
FROM activitypub_actor_property
WHERE actor_id = ?`;
if (options.names) {
sql += ` AND prop_name IN("${options.names.join('","')}");`;
} else {
sql += ';';
}
let properties = {};
actorDb.each(
sql,
[actorId],
(err, row) => {
if (err) {
return cb(err);
}
properties[row.prop_name] = row.prop_value;
},
err => {
return cb(err, err ? null : properties);
}
);
}
// :TODO: make this much more flexible - propValue should allow for case-insensitive compare, etc.
static getActorIdsWithProperty(propName, propValue, cb) {
let actorIds = [];
actorDb.each(
`SELECT actor_id
FROM activitypub_actor_property
WHERE prop_name = ? AND prop_value = ?;`,
[propName, propValue],
(err, row) => {
if (row) {
actorIds.push(row.actor_id);
}
},
() => {
return cb(null, actorIds);
}
);
}
static getActorCount(cb) {
actorDb.get(
`SELECT count() AS actor_count
FROM activitypub_actor;`,
(err, row) => {
if (err) {
return cb(err);
}
return cb(null, row.actor_count);
}
);
}
};

View File

@ -0,0 +1,18 @@
/* jslint node: true */
'use strict';
//
// Common Activitypub actor properties used throughout the system.
//
// This IS NOT a full list. For example, custom modules
// can utilize their own properties as well!
//
module.exports = {
Type: 'type',
PreferredUsername: 'preferred_user_name',
Name: 'name',
Summary: 'summary',
IconUrl: 'icon_url',
BannerUrl: 'banner_url',
PublicKeyMain: 'public_key_main_rsa_pem', // RSA public key for user
};

View File

@ -41,7 +41,7 @@ function getModDatabasePath(moduleInfo, suffix) {
// filename. An optional suffix may be supplied as well.
//
const HOST_RE =
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
assert(_.isObject(moduleInfo));
assert(_.isString(moduleInfo.packageName), 'moduleInfo must define "packageName"!');
@ -81,7 +81,7 @@ function getISOTimestampString(ts) {
function sanitizeString(s) {
return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => {
// eslint-disable-line no-control-regex
// eslint-disable-line no-control-regex
switch (c) {
case '\0':
return '\\0';
@ -97,7 +97,7 @@ function sanitizeString(s) {
return '\\r';
case '"':
case "'":
case '\'':
return `${c}${c}`;
case '\\':
@ -109,7 +109,7 @@ function sanitizeString(s) {
function initializeDatabases(cb) {
async.eachSeries(
['system', 'user', 'message', 'file'],
['system', 'user', 'actor', 'message', 'file'],
(dbName, next) => {
dbs[dbName] = sqlite3Trans.wrap(
new sqlite3.Database(getDatabasePath(dbName), err => {
@ -242,6 +242,36 @@ const DB_INIT_TABLE = {
return cb(null);
},
actor: cb => {
enableForeignKeys(dbs.actor);
dbs.actor.run(
`CREATE TABLE IF NOT EXISTS activitypub_actor (
id INTEGER PRIMARY KEY,
actor_url VARCHAR NOT NULL,
UNIQUE(actor_url)
);`
);
// :TODO: create FK on delete/etc.
dbs.actor.run(
`CREATE TABLE IF NOT EXISTS activitypub_actor_property (
actor_id INTEGER NOT NULL,
prop_name VARCHAR NOT NULL,
prop_value VARCHAR,
UNIQUE(actor_id, prop_name),
FOREIGN KEY(actor_id) REFERENCES actor(id) ON DELETE CASCADE
);`
);
dbs.actor.run(
`CREATE INDEX IF NOT EXISTS activitypub_actor_property_id_and_name_index0
ON activitypub_actor_property (actor_id, prop_name);`
);
return cb(null);
},
message: cb => {
enableForeignKeys(dbs.message);
@ -312,22 +342,22 @@ const DB_INIT_TABLE = {
// :TODO: need SQL to ensure cleaned up if delete from message?
/*
dbs.message.run(
`CREATE TABLE IF NOT EXISTS hash_tag (
hash_tag_id INTEGER PRIMARY KEY,
hash_tag_name VARCHAR NOT NULL,
UNIQUE(hash_tag_name)
);`
);
dbs.message.run(
`CREATE TABLE IF NOT EXISTS hash_tag (
hash_tag_id INTEGER PRIMARY KEY,
hash_tag_name VARCHAR NOT NULL,
UNIQUE(hash_tag_name)
);`
);
// :TODO: need SQL to ensure cleaned up if delete from message?
dbs.message.run(
`CREATE TABLE IF NOT EXISTS message_hash_tag (
hash_tag_id INTEGER NOT NULL,
message_id INTEGER NOT NULL,
);`
);
*/
// :TODO: need SQL to ensure cleaned up if delete from message?
dbs.message.run(
`CREATE TABLE IF NOT EXISTS message_hash_tag (
hash_tag_id INTEGER NOT NULL,
message_id INTEGER NOT NULL,
);`
);
*/
dbs.message.run(
`CREATE TABLE IF NOT EXISTS user_message_area_last_read (