Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ElectronPlatform: Add support for a event index using Seshat. #11125

Merged
merged 32 commits into from
Nov 26, 2019
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1dbdd0a
ElectronPlatform: Add support for a event index using Seshat.
poljar Oct 11, 2019
71023ae
ElectronPlatform: Fix lint errors.
poljar Oct 11, 2019
94196eb
electron-main: Use camle-case for the send_error method.
poljar Nov 8, 2019
a6839af
electron-main: Use a capital letter for the seshat import.
poljar Nov 8, 2019
c3c5756
ElectronPlatform: Implement the EventIndexManager for Seshat.
poljar Nov 13, 2019
449eca6
electron-main: Add a missing break.
poljar Nov 13, 2019
437c59f
electron-main: Check for seshat existence instead of erroring out.
poljar Nov 13, 2019
e9352fc
electron-main: Switch to matrix-seshat.
poljar Nov 14, 2019
b90a94b
electron-main: Enable encryption for Seshat.
poljar Nov 14, 2019
7147af8
ElectronPlatform: Don't scope the event index per user.
poljar Nov 14, 2019
dd2c210
electron-main: Rework the event index initialization and deletion.
poljar Nov 14, 2019
076bf6f
develop: Enable the event indexing feature in labs.
poljar Nov 19, 2019
0813aff
electron-main: Remove an extra newline.
poljar Nov 19, 2019
b17a403
electron-main: No need to normalize the path.
poljar Nov 19, 2019
4a25252
ElectronPlatform: Rename the SeshatIndexerManager.
poljar Nov 19, 2019
73b302f
ElectronPlatform: Fix some type annotations.
poljar Nov 19, 2019
137bedb
ElectronPlatform: Update the path for the BaseEventIndexManager class.
poljar Nov 19, 2019
2f2cbad
electron_app: Remove Seshat from the dependencies.
poljar Nov 21, 2019
d0b5391
docs: Add documentation explaining how to enable Seshat support.
poljar Nov 21, 2019
e96c44c
package.json: Remove the unneeded Neon/Seshat dependencies.
poljar Nov 26, 2019
4c629e8
native-node-modules: Add a header level to the title.
poljar Nov 26, 2019
da4b403
native-node-modules: Don't mention the riot version that supports nat…
poljar Nov 26, 2019
b52141d
native-node-modules: Add a section about cross compilation.
poljar Nov 26, 2019
5f6636e
native-node-modules: Reword the second paragraph.
poljar Nov 26, 2019
b1aff29
native-node-modules: Explain the packaging situation a bit.
poljar Nov 26, 2019
40f2648
native-node-modules: Expand the Seshat subtitle a bit.
poljar Nov 26, 2019
f0fe968
native-node-modules: Capitalize some project names.
poljar Nov 26, 2019
5b8e918
native-node-modules: Explain how to install Rust and link to its docs.
poljar Nov 26, 2019
1869350
native-node-modules: Remove a spurious and.
poljar Nov 26, 2019
b0783a8
native-node-modules: Mention that Seshat requires SQLCipher.
poljar Nov 26, 2019
f28f27a
labs: Document the event indexing labs feature.
poljar Nov 26, 2019
e5956de
labs: Clarify that the event indexing feature supports E2EE search.
poljar Nov 26, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions docs/native-node-modules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Native Node Modules

For some features, the desktop version of Riot can make use of native Node
modules. These allow Riot to integrate with the desktop in ways that a browser
cannot.

While native modules enable powerful new features, they must be complied for
each operating system. For official Riot releases, we will always build these
modules from source to ensure we can trust the compiled output. In the future,
we may offer a pre-compiled path for those who want to use these features in a
custom build of Riot without installing the various build tools required.

Do note that compiling a module for a particular operating system
(Linux/macOS/Windows) will need to be done on that operating system.
Cross-compiling from a host OS for a different target OS may be possible, but
we don't support this flow with Riot dependencies at this time.

jryans marked this conversation as resolved.
Show resolved Hide resolved
At the moment, we need to make some changes to the Riot release process before
we can support native Node modules at release time, so these features are
currently disabled by default until that is resolved. The following sections
explain the manual steps you can use with a custom build of Riot to enable
these features if you'd like to try them out.

## Adding Seshat for search in E2E encrypted rooms

Seshat is a native Node module that adds support for local event indexing and
full text search in E2E encrypted rooms.

Since Seshat is written in Rust, the Rust compiler and related tools need to be
installed before installing Seshat itself. To install Rust please consult the
official Rust [documentation](https://www.rust-lang.org/tools/install).

After installing the compiler, Seshat support can be added using yarn inside
the `electron_app/` directory:

yarn add matrix-seshat
jryans marked this conversation as resolved.
Show resolved Hide resolved

After this is done the Electron version of Riot can be run from the main folder
as usual using:

yarn electron

If for some reason recompilation of Seshat is needed, e.g. when using a
development version of Seshat using `yarn link`, or if the initial compilation was
done for the wrong electron version, Seshat can be recompiled with the
`electron-build-env` tool. Again from the `electron_app/` directory:

yarn add electron-build-env

Recompiling Seshat itself can be done like so:

yarn run electron-build-env -- --electron 6.1.1 -- neon build matrix-seshat --release`

Please make sure to include all the `--` as well as the `--release` command line
switch at the end. Modify your electron version accordingly depending on the
version that is installed on your system.
Comment on lines +46 to +59
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, so is electron-build-env needed even for the first installation of the matrix-seshat Node module, or is only a rebuild helper?

If it's only used for rebuilds, I wonder if we can find some way to repeat the build that happens at install time without this extra tool somehow, such as (cd node_modules/matrix-seshat; yarn run install) or similar.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also noticed that the Electron docs suggest electron-rebuild, is that of any use here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it's only needed to rebuild it, but I'm not 100% on that.

The neon docs state this:

We are working on adding Neon support to electron-rebuild, so you'll be able to just drop Neon dependencies into your app like any other. For now, there's a tool called electron-build-env that you can use for building any Neon dependencies of your Electron app.

As far as I can tell this has not changed as of yet.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After looking around a bit, it's quite difficult to follow what's actually recommended and also matches what we need here. 😓 I don't want to block all this work on sorting it out though, so I filed a separate issue to improve this in a future step.

162 changes: 162 additions & 0 deletions electron_app/src/electron-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ const { migrateFromOldOrigin } = require('./originMigrator');
const windowStateKeeper = require('electron-window-state');
const Store = require('electron-store');

const fs = require('fs');
const afs = fs.promises;

let Seshat = null;

try {
Seshat = require('matrix-seshat');
} catch (e) {
}

if (argv["help"]) {
console.log("Options:");
console.log(" --profile-dir {path}: Path to where to store the profile.");
Expand Down Expand Up @@ -82,8 +92,11 @@ try {
// Could not load local config, this is expected in most cases.
}

const eventStorePath = path.join(app.getPath('userData'), 'EventStore');
const store = new Store({ name: "electron-config" });

let eventIndex = null;

let mainWindow = null;
global.appQuitting = false;
global.minimizeToTray = store.get('minimizeToTray', true);
Expand Down Expand Up @@ -200,6 +213,7 @@ ipcMain.on('ipcCall', async function(ev, payload) {
case 'getConfig':
ret = vectorConfig;
break;

default:
mainWindow.webContents.send('ipcReply', {
id: payload.id,
Expand All @@ -214,6 +228,154 @@ ipcMain.on('ipcCall', async function(ev, payload) {
});
});

ipcMain.on('seshat', async function(ev, payload) {
if (!mainWindow) return;

const sendError = (id, e) => {
const error = {
message: e.message
}

mainWindow.webContents.send('seshatReply', {
id:id,
error: error
});
}

const args = payload.args || [];
let ret;

switch (payload.name) {
case 'supportsEventIndexing':
if (Seshat === null) ret = false;
else ret = true;
break;

case 'initEventIndex':
if (eventIndex === null) {
try {
await afs.mkdir(eventStorePath, {recursive: true});
eventIndex = new Seshat(eventStorePath, {passphrase: "DEFAULT_PASSPHRASE"});
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;

case 'closeEventIndex':
eventIndex = null;
break;

case 'deleteEventIndex':
const deleteFolderRecursive = async(p) => {
for (let entry of await afs.readdir(p)) {
const curPath = path.join(p, entry);
await afs.unlink(curPath);
}
}

try {
await deleteFolderRecursive(eventStorePath);
} catch (e) {
jryans marked this conversation as resolved.
Show resolved Hide resolved
}

break;

case 'isEventIndexEmpty':
if (eventIndex === null) ret = true;
else ret = await eventIndex.isEmpty();
break;

case 'addEventToIndex':
try {
eventIndex.addEvent(args[0], args[1]);
} catch (e) {
sendError(payload.id, e);
return;
}
break;

case 'commitLiveEvents':
try {
ret = await eventIndex.commit();
} catch (e) {
sendError(payload.id, e);
return;
}
break;

case 'searchEventIndex':
try {
ret = await eventIndex.search(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
break;

case 'addHistoricEvents':
if (eventIndex === null) ret = false;
else {
try {
ret = await eventIndex.addHistoricEvents(
args[0], args[1], args[2]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;

case 'removeCrawlerCheckpoint':
if (eventIndex === null) ret = false;
else {
try {
ret = await eventIndex.removeCrawlerCheckpoint(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;

case 'addCrawlerCheckpoint':
if (eventIndex === null) ret = false;
else {
try {
ret = await eventIndex.addCrawlerCheckpoint(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;

case 'loadCheckpoints':
if (eventIndex === null) ret = [];
else {
try {
ret = await eventIndex.loadCheckpoints();
} catch (e) {
ret = [];
}
}
break;

default:
mainWindow.webContents.send('seshatReply', {
id: payload.id,
error: "Unknown IPC Call: " + payload.name,
});
return;
}

mainWindow.webContents.send('seshatReply', {
id: payload.id,
reply: ret,
});
});

app.commandLine.appendSwitch('--enable-usermedia-screen-capturing');

const gotLock = app.requestSingleInstanceLock();
Expand Down
3 changes: 2 additions & 1 deletion riot.im/develop/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"feature_sas": "labs",
"feature_room_breadcrumbs": "labs",
"feature_state_counters": "labs",
"feature_many_integration_managers": "labs"
"feature_many_integration_managers": "labs",
"feature_event_indexing": "labs"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also add a short explanation of this flag in the labs docs as per the guide.

},
"welcomeUserId": "@riot-bot:matrix.org",
"piwik": {
Expand Down
97 changes: 97 additions & 0 deletions src/vector/platform/ElectronPlatform.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ limitations under the License.
*/

import VectorBasePlatform, {updateCheckStatusEnum} from './VectorBasePlatform';
import BaseEventIndexManager from 'matrix-react-sdk/lib/indexing/BaseEventIndexManager';
import dis from 'matrix-react-sdk/lib/dispatcher';
import { _t } from 'matrix-react-sdk/lib/languageHandler';
import Promise from 'bluebird';
Expand Down Expand Up @@ -66,12 +67,104 @@ function getUpdateCheckStatus(status) {
}
}

class SeshatIndexManager extends BaseEventIndexManager {
constructor() {
super();

this._pendingIpcCalls = {};
this._nextIpcCallId = 0;
ipcRenderer.on('seshatReply', this._onIpcReply.bind(this));
}

async _ipcCall(name: string, ...args: []): Promise<{}> {
// TODO this should be moved into the preload.js file.
const ipcCallId = ++this._nextIpcCallId;
return new Promise((resolve, reject) => {
this._pendingIpcCalls[ipcCallId] = {resolve, reject};
window.ipcRenderer.send('seshat', {id: ipcCallId, name, args});
});
}

_onIpcReply(ev: {}, payload: {}) {
if (payload.id === undefined) {
console.warn("Ignoring IPC reply with no ID");
return;
}

if (this._pendingIpcCalls[payload.id] === undefined) {
console.warn("Unknown IPC payload ID: " + payload.id);
return;
}

const callbacks = this._pendingIpcCalls[payload.id];
delete this._pendingIpcCalls[payload.id];
if (payload.error) {
callbacks.reject(payload.error);
} else {
callbacks.resolve(payload.reply);
}
}

async supportsEventIndexing(): Promise<boolean> {
return this._ipcCall('supportsEventIndexing');
}

async initEventIndex(): Promise<> {
return this._ipcCall('initEventIndex');
}

async addEventToIndex(ev: MatrixEvent, profile: MatrixProfile): Promise<> {
return this._ipcCall('addEventToIndex', ev, profile);
}

async isEventIndexEmpty(): Promise<boolean> {
return this._ipcCall('isEventIndexEmpty');
}

async commitLiveEvents(): Promise<> {
return this._ipcCall('commitLiveEvents');
}

async searchEventIndex(searchConfig: SearchConfig): Promise<SearchResult> {
return this._ipcCall('searchEventIndex', searchConfig);
}

async addHistoricEvents(
events: [HistoricEvent],
checkpoint: CrawlerCheckpoint | null,
oldCheckpoint: CrawlerCheckpoint | null,
): Promise<> {
return this._ipcCall('addHistoricEvents', events, checkpoint, oldCheckpoint);
}

async addCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<> {
return this._ipcCall('addCrawlerCheckpoint', checkpoint);
}

async removeCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<> {
return this._ipcCall('removeCrawlerCheckpoint', checkpoint);
}

async loadCheckpoints(): Promise<[CrawlerCheckpoint]> {
return this._ipcCall('loadCheckpoints');
}

async closeEventIndex(): Promise<> {
return this._ipcCall('closeEventIndex');
}

async deleteEventIndex(): Promise<> {
return this._ipcCall('deleteEventIndex');
}
}

export default class ElectronPlatform extends VectorBasePlatform {
constructor() {
super();

this._pendingIpcCalls = {};
this._nextIpcCallId = 0;
this.eventIndexManager = new SeshatIndexManager();

dis.register(_onAction);
/*
Expand Down Expand Up @@ -293,4 +386,8 @@ export default class ElectronPlatform extends VectorBasePlatform {
callbacks.resolve(payload.reply);
}
}

getEventIndexingManager(): BaseEventIndexManager | null {
return this.eventIndexManager;
}
}