From 9eb3a1d37f9127df5c70c521fd76139882fb72b0 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Sat, 7 Jan 2023 13:40:52 -0600 Subject: [PATCH 1/2] eslint fixes as well as fixing a small variable typo --- .../content/web_handlers/activitypub.js | 4 +- .../servers/content/web_handlers/webfinger.js | 422 +++++++++--------- 2 files changed, 211 insertions(+), 215 deletions(-) diff --git a/core/servers/content/web_handlers/activitypub.js b/core/servers/content/web_handlers/activitypub.js index 57a16b14..d81d8cce 100644 --- a/core/servers/content/web_handlers/activitypub.js +++ b/core/servers/content/web_handlers/activitypub.js @@ -8,12 +8,10 @@ const { DefaultProfileTemplate, } = require('../../../activitypub_util'); const UserProps = require('../../../user_property'); -const { Errors } = require('../../../enig_error'); const Config = require('../../../config').get; // deps const _ = require('lodash'); -const { trim } = require('lodash'); const enigma_assert = require('../../../enigma_assert'); exports.moduleInfo = { @@ -59,7 +57,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { userFromAccount(accountName, (err, user) => { if (err) { this.log.info( - { reason: error.message, accountName: accountName }, + { reason: err.message, accountName: accountName }, `No user "${accountName}" for "self"` ); return this._notFound(resp); diff --git a/core/servers/content/web_handlers/webfinger.js b/core/servers/content/web_handlers/webfinger.js index a7cf6e82..8b695773 100644 --- a/core/servers/content/web_handlers/webfinger.js +++ b/core/servers/content/web_handlers/webfinger.js @@ -3,242 +3,240 @@ const Config = require('../../../config').get; const { Errors } = require('../../../enig_error'); const { WellKnownLocations } = require('../web'); const { - selfUrl, - webFingerProfileUrl, - userFromAccount, - getUserProfileTemplatedBody, - DefaultProfileTemplate, + selfUrl, + webFingerProfileUrl, + userFromAccount, + getUserProfileTemplatedBody, + DefaultProfileTemplate, } = require('../../../activitypub_util'); const _ = require('lodash'); const enigma_assert = require('../../../enigma_assert'); exports.moduleInfo = { - name: 'WebFinger', - desc: 'A simple WebFinger Handler.', - author: 'NuSkooler, CognitiveGears', - packageName: 'codes.l33t.enigma.web.handler.webfinger', + name: 'WebFinger', + desc: 'A simple WebFinger Handler.', + author: 'NuSkooler, CognitiveGears', + packageName: 'codes.l33t.enigma.web.handler.webfinger', }; // // WebFinger: https://www.rfc-editor.org/rfc/rfc7033 // exports.getModule = class WebFingerWebHandler extends WebHandlerModule { - constructor() { - super(); + constructor() { + super(); + } + + init(webServer, cb) { + // we rely on the web server + this.webServer = webServer; + enigma_assert(webServer, 'WebFinger Web Handler init without webServer'); + + this.log = webServer.logger().child({ webHandler: 'WebFinger' }); + + const domain = this.webServer.getDomain(); + if (!domain) { + return cb(Errors.UnexpectedState('Web server does not have "domain" set')); } - init(webServer, cb) { - const config = Config(); + this.acceptedResourceRegExps = [ + // acct:NAME@our.domain.tld + // https://www.rfc-editor.org/rfc/rfc7565 + new RegExp(`^acct:(.+)@${domain}$`), + // profile page + // https://webfinger.net/rel/profile-page/ + new RegExp( + `^${this.webServer.buildUrl(WellKnownLocations.Internal + '/wf/@')}(.+)$` + ), + // self URL + new RegExp( + `^${this.webServer.buildUrl( + WellKnownLocations.Internal + '/ap/users/' + )}(.+)$` + ), + ]; - // we rely on the web server - this.webServer = webServer; - enigma_assert(webServer, 'WebFinger Web Handler init without webServer'); + this.webServer.addRoute({ + method: 'GET', + // https://www.rfc-editor.org/rfc/rfc7033.html#section-10.1 + path: /^\/\.well-known\/webfinger\/?\?/, + handler: this._webFingerRequestHandler.bind(this), + }); - this.log = webServer.logger().child({ webHandler: 'WebFinger' }); + this.webServer.addRoute({ + method: 'GET', + path: /^\/_enig\/wf\/@.+$/, + handler: this._profileRequestHandler.bind(this), + }); - const domain = this.webServer.getDomain(); - if (!domain) { - return cb(Errors.UnexpectedState('Web server does not have "domain" set')); - } + return cb(null); + } - this.acceptedResourceRegExps = [ - // acct:NAME@our.domain.tld - // https://www.rfc-editor.org/rfc/rfc7565 - new RegExp(`^acct:(.+)@${domain}$`), - // profile page - // https://webfinger.net/rel/profile-page/ - new RegExp( - `^${this.webServer.buildUrl(WellKnownLocations.Internal + '/wf/@')}(.+)$` - ), - // self URL - new RegExp( - `^${this.webServer.buildUrl( - WellKnownLocations.Internal + '/ap/users/' - )}(.+)$` - ), - ]; + _profileRequestHandler(req, resp) { + const url = new URL(req.url, `https://${req.headers.host}`); - this.webServer.addRoute({ - method: 'GET', - // https://www.rfc-editor.org/rfc/rfc7033.html#section-10.1 - path: /^\/\.well-known\/webfinger\/?\?/, - handler: this._webFingerRequestHandler.bind(this), - }); - - this.webServer.addRoute({ - method: 'GET', - path: /^\/_enig\/wf\/@.+$/, - handler: this._profileRequestHandler.bind(this), - }); - - return cb(null); + const resource = url.pathname; + if (_.isEmpty(resource)) { + return this.webServer.instance.respondWithError( + resp, + 400, + 'pathname is required', + 'Missing "resource"' + ); } - _profileRequestHandler(req, resp) { - const url = new URL(req.url, `https://${req.headers.host}`); - - const resource = url.pathname; - if (_.isEmpty(resource)) { - return this.webServer.instance.respondWithError( - resp, - 400, - 'pathname is required', - 'Missing "resource"' - ); - } - - const userPosition = resource.indexOf('@'); - if (-1 == userPosition || userPosition == resource.length - 1) { - this._notFound(resp); - return Errors.DoesNotExist('"@username" missing from path'); - } - - const accountName = resource.substring(userPosition + 1); - - userFromAccount(accountName, (err, user) => { - if (err) { - this.log.warn( - { url: req.url, error: err.message, type: 'Profile' }, - `No profile for "${accountName}" could be retrieved` - ); - return this._notFound(resp); - } - - let templateFile = _.get( - Config(), - 'contentServers.web.handlers.webFinger.profileTemplate' - ); - if (templateFile) { - templateFile = this.webServer.resolveTemplatePath(templateFile); - } - - getUserProfileTemplatedBody( - templateFile, - user, - DefaultProfileTemplate, - 'text/plain', - (err, body, contentType) => { - if (err) { - return this._notFound(resp); - } - - const headers = { - 'Content-Type': contentType, - 'Content-Length': body.length, - }; - - resp.writeHead(200, headers); - return resp.end(body); - } - ); - }); + const userPosition = resource.indexOf('@'); + if (-1 == userPosition || userPosition == resource.length - 1) { + this._notFound(resp); + return Errors.DoesNotExist('"@username" missing from path'); } - _webFingerRequestHandler(req, resp) { - const url = new URL(req.url, `https://${req.headers.host}`); + const accountName = resource.substring(userPosition + 1); - const resource = url.searchParams.get('resource'); - if (!resource) { - return this.webServer.respondWithError( - resp, - 400, - '"resource" is required', - 'Missing "resource"' - ); - } - - const accountName = this._getAccountName(resource); - if (!accountName || accountName.length < 1) { - this._notFound(resp); - return Errors.DoesNotExist( - `Failed to parse "account name" for resource: ${resource}` - ); - } - - userFromAccount(accountName, (err, user) => { - if (err) { - this.log.warn( - { url: req.url, error: err.message, type: 'WebFinger' }, - `No account for "${accountName}" could be retrieved` - ); - return this._notFound(resp); - } - - const domain = this.webServer.getDomain(); - - const body = JSON.stringify({ - subject: `acct:${user.username}@${domain}`, - aliases: [this._profileUrl(user), this._selfUrl(user)], - links: [ - this._profilePageLink(user), - this._selfLink(user), - this._subscribeLink(), - ], - }); - - const headers = { - 'Content-Type': 'application/jrd+json', - 'Content-Length': body.length, - }; - - resp.writeHead(200, headers); - return resp.end(body); - }); - } - - _profileUrl(user) { - return webFingerProfileUrl(this.webServer, user); - } - - _profilePageLink(user) { - const href = this._profileUrl(user); - return { - rel: 'http://webfinger.net/rel/profile-page', - type: 'text/plain', - href, - }; - } - - _selfUrl(user) { - return selfUrl(this.webServer, user); - } - - // :TODO: only if ActivityPub is enabled - _selfLink(user) { - const href = this._selfUrl(user); - return { - rel: 'self', - type: 'application/activity+json', - href, - }; - } - - // :TODO: only if ActivityPub is enabled - _subscribeLink() { - return { - rel: 'http://ostatus.org/schema/1.0/subscribe', - template: this.webServer.buildUrl( - WellKnownLocations.Internal + '/ap/authorize_interaction?uri={uri}' - ), - }; - } - - _getAccountName(resource) { - for (const re of this.acceptedResourceRegExps) { - const m = resource.match(re); - if (m && m.length === 2) { - return m[1]; - } - } - } - - _notFound(resp) { - this.webServer.respondWithError( - resp, - 404, - 'Resource not found', - 'Resource Not Found' + userFromAccount(accountName, (err, user) => { + if (err) { + this.log.warn( + { url: req.url, error: err.message, type: 'Profile' }, + `No profile for "${accountName}" could be retrieved` ); + return this._notFound(resp); + } + + let templateFile = _.get( + Config(), + 'contentServers.web.handlers.webFinger.profileTemplate' + ); + if (templateFile) { + templateFile = this.webServer.resolveTemplatePath(templateFile); + } + + getUserProfileTemplatedBody( + templateFile, + user, + DefaultProfileTemplate, + 'text/plain', + (err, body, contentType) => { + if (err) { + return this._notFound(resp); + } + + const headers = { + 'Content-Type': contentType, + 'Content-Length': body.length, + }; + + resp.writeHead(200, headers); + return resp.end(body); + } + ); + }); + } + + _webFingerRequestHandler(req, resp) { + const url = new URL(req.url, `https://${req.headers.host}`); + + const resource = url.searchParams.get('resource'); + if (!resource) { + return this.webServer.respondWithError( + resp, + 400, + '"resource" is required', + 'Missing "resource"' + ); } + + const accountName = this._getAccountName(resource); + if (!accountName || accountName.length < 1) { + this._notFound(resp); + return Errors.DoesNotExist( + `Failed to parse "account name" for resource: ${resource}` + ); + } + + userFromAccount(accountName, (err, user) => { + if (err) { + this.log.warn( + { url: req.url, error: err.message, type: 'WebFinger' }, + `No account for "${accountName}" could be retrieved` + ); + return this._notFound(resp); + } + + const domain = this.webServer.getDomain(); + + const body = JSON.stringify({ + subject: `acct:${user.username}@${domain}`, + aliases: [this._profileUrl(user), this._selfUrl(user)], + links: [ + this._profilePageLink(user), + this._selfLink(user), + this._subscribeLink(), + ], + }); + + const headers = { + 'Content-Type': 'application/jrd+json', + 'Content-Length': body.length, + }; + + resp.writeHead(200, headers); + return resp.end(body); + }); + } + + _profileUrl(user) { + return webFingerProfileUrl(this.webServer, user); + } + + _profilePageLink(user) { + const href = this._profileUrl(user); + return { + rel: 'http://webfinger.net/rel/profile-page', + type: 'text/plain', + href, + }; + } + + _selfUrl(user) { + return selfUrl(this.webServer, user); + } + + // :TODO: only if ActivityPub is enabled + _selfLink(user) { + const href = this._selfUrl(user); + return { + rel: 'self', + type: 'application/activity+json', + href, + }; + } + + // :TODO: only if ActivityPub is enabled + _subscribeLink() { + return { + rel: 'http://ostatus.org/schema/1.0/subscribe', + template: this.webServer.buildUrl( + WellKnownLocations.Internal + '/ap/authorize_interaction?uri={uri}' + ), + }; + } + + _getAccountName(resource) { + for (const re of this.acceptedResourceRegExps) { + const m = resource.match(re); + if (m && m.length === 2) { + return m[1]; + } + } + } + + _notFound(resp) { + this.webServer.respondWithError( + resp, + 404, + 'Resource not found', + 'Resource Not Found' + ); + } }; From 2c0992becb43206e07cc06c0b052880652d4abad Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Sat, 7 Jan 2023 14:48:12 -0600 Subject: [PATCH 2/2] Added stub activitypub_actor database entries --- .eslintrc.json | 3 +- core/activitypub_actor.js | 383 +++++++++++++++++++++++++++++ core/activitypub_actor_property.js | 18 ++ core/database.js | 68 +++-- 4 files changed, 452 insertions(+), 20 deletions(-) create mode 100644 core/activitypub_actor.js create mode 100644 core/activitypub_actor_property.js diff --git a/.eslintrc.json b/.eslintrc.json index e19846d8..b22b711c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -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 diff --git a/core/activitypub_actor.js b/core/activitypub_actor.js new file mode 100644 index 00000000..9ab8f01b --- /dev/null +++ b/core/activitypub_actor.js @@ -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); + } + ); + } +}; diff --git a/core/activitypub_actor_property.js b/core/activitypub_actor_property.js new file mode 100644 index 00000000..8727b830 --- /dev/null +++ b/core/activitypub_actor_property.js @@ -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 +}; diff --git a/core/database.js b/core/database.js index 9266001d..8c579507 100644 --- a/core/database.js +++ b/core/database.js @@ -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 (