Merge
This commit is contained in:
commit
f9f9208ada
|
@ -16,7 +16,8 @@
|
||||||
"quotes": ["error", "single"],
|
"quotes": ["error", "single"],
|
||||||
"semi": ["error", "always"],
|
"semi": ["error", "always"],
|
||||||
"comma-dangle": 0,
|
"comma-dangle": 0,
|
||||||
"no-trailing-spaces": "error"
|
"no-trailing-spaces": "error",
|
||||||
|
"no-control-regex": 0
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2020
|
"ecmaVersion": 2020
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
|
@ -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
|
||||||
|
};
|
|
@ -41,7 +41,7 @@ function getModDatabasePath(moduleInfo, suffix) {
|
||||||
// filename. An optional suffix may be supplied as well.
|
// filename. An optional suffix may be supplied as well.
|
||||||
//
|
//
|
||||||
const HOST_RE =
|
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(_.isObject(moduleInfo));
|
||||||
assert(_.isString(moduleInfo.packageName), 'moduleInfo must define "packageName"!');
|
assert(_.isString(moduleInfo.packageName), 'moduleInfo must define "packageName"!');
|
||||||
|
@ -81,7 +81,7 @@ function getISOTimestampString(ts) {
|
||||||
|
|
||||||
function sanitizeString(s) {
|
function sanitizeString(s) {
|
||||||
return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => {
|
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) {
|
switch (c) {
|
||||||
case '\0':
|
case '\0':
|
||||||
return '\\0';
|
return '\\0';
|
||||||
|
@ -97,7 +97,7 @@ function sanitizeString(s) {
|
||||||
return '\\r';
|
return '\\r';
|
||||||
|
|
||||||
case '"':
|
case '"':
|
||||||
case "'":
|
case '\'':
|
||||||
return `${c}${c}`;
|
return `${c}${c}`;
|
||||||
|
|
||||||
case '\\':
|
case '\\':
|
||||||
|
@ -109,7 +109,7 @@ function sanitizeString(s) {
|
||||||
|
|
||||||
function initializeDatabases(cb) {
|
function initializeDatabases(cb) {
|
||||||
async.eachSeries(
|
async.eachSeries(
|
||||||
['system', 'user', 'message', 'file'],
|
['system', 'user', 'actor', 'message', 'file'],
|
||||||
(dbName, next) => {
|
(dbName, next) => {
|
||||||
dbs[dbName] = sqlite3Trans.wrap(
|
dbs[dbName] = sqlite3Trans.wrap(
|
||||||
new sqlite3.Database(getDatabasePath(dbName), err => {
|
new sqlite3.Database(getDatabasePath(dbName), err => {
|
||||||
|
@ -242,6 +242,36 @@ const DB_INIT_TABLE = {
|
||||||
|
|
||||||
return cb(null);
|
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 => {
|
message: cb => {
|
||||||
enableForeignKeys(dbs.message);
|
enableForeignKeys(dbs.message);
|
||||||
|
@ -312,22 +342,22 @@ const DB_INIT_TABLE = {
|
||||||
|
|
||||||
// :TODO: need SQL to ensure cleaned up if delete from message?
|
// :TODO: need SQL to ensure cleaned up if delete from message?
|
||||||
/*
|
/*
|
||||||
dbs.message.run(
|
dbs.message.run(
|
||||||
`CREATE TABLE IF NOT EXISTS hash_tag (
|
`CREATE TABLE IF NOT EXISTS hash_tag (
|
||||||
hash_tag_id INTEGER PRIMARY KEY,
|
hash_tag_id INTEGER PRIMARY KEY,
|
||||||
hash_tag_name VARCHAR NOT NULL,
|
hash_tag_name VARCHAR NOT NULL,
|
||||||
UNIQUE(hash_tag_name)
|
UNIQUE(hash_tag_name)
|
||||||
);`
|
);`
|
||||||
);
|
);
|
||||||
|
|
||||||
// :TODO: need SQL to ensure cleaned up if delete from message?
|
// :TODO: need SQL to ensure cleaned up if delete from message?
|
||||||
dbs.message.run(
|
dbs.message.run(
|
||||||
`CREATE TABLE IF NOT EXISTS message_hash_tag (
|
`CREATE TABLE IF NOT EXISTS message_hash_tag (
|
||||||
hash_tag_id INTEGER NOT NULL,
|
hash_tag_id INTEGER NOT NULL,
|
||||||
message_id INTEGER NOT NULL,
|
message_id INTEGER NOT NULL,
|
||||||
);`
|
);`
|
||||||
);
|
);
|
||||||
*/
|
*/
|
||||||
|
|
||||||
dbs.message.run(
|
dbs.message.run(
|
||||||
`CREATE TABLE IF NOT EXISTS user_message_area_last_read (
|
`CREATE TABLE IF NOT EXISTS user_message_area_last_read (
|
||||||
|
|
Loading…
Reference in New Issue