optional encryption

This commit is contained in:
Evert Prants 2023-08-02 20:25:37 +03:00
parent d6ecd65a81
commit 11958b8db0
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
6 changed files with 1573 additions and 673 deletions

View File

@ -3,7 +3,7 @@
"name": "matrix", "name": "matrix",
"description": "Matrix.org Service for Squeebot 3", "description": "Matrix.org Service for Squeebot 3",
"tags": ["service", "matrix"], "tags": ["service", "matrix"],
"version": "1.0.3", "version": "1.2.0",
"dependencies": ["control?"], "dependencies": ["control?"],
"npmDependencies": ["matrix-bot-sdk@0.5.19"] "npmDependencies": ["matrix-bot-sdk@0.6.6"]
} }

View File

@ -2,31 +2,38 @@ import {
Plugin, Plugin,
Configurable, Configurable,
EventListener, EventListener,
InjectService InjectService,
} from '@squeebot/core/lib/plugin'; } from '@squeebot/core/lib/plugin';
import util from 'util'; import util from 'util';
import { promises as fs } from 'fs';
import { import {
MatrixClient, MatrixClient,
AutojoinRoomsMixin, AutojoinRoomsMixin,
RichReply,
SimpleFsStorageProvider, SimpleFsStorageProvider,
LogService, LogService,
LogLevel, LogLevel,
RustSdkCryptoStorageProvider,
} from 'matrix-bot-sdk'; } from 'matrix-bot-sdk';
import { StoreType } from '@matrix-org/matrix-sdk-crypto-nodejs';
import { logger } from '@squeebot/core/lib/core'; import { Logger, logger } from '@squeebot/core/lib/core';
import { import {
EMessageType, EMessageType,
Formatter, Formatter,
IMessage, IMessage,
IMessageTarget, IMessageTarget,
Protocol Protocol,
ProtocolFeatureFlag,
} from '@squeebot/core/lib/types'; } from '@squeebot/core/lib/types';
import { MatrixFormatter } from './format'; import { MatrixFormatter } from './format';
import { resolve, join } from 'path';
/**
* Message adapter for Matrix protocol.
*/
class MatrixMessageAdapter implements IMessage { class MatrixMessageAdapter implements IMessage {
public time: Date = new Date(); public time: Date = new Date();
public resolved = false; public resolved = false;
@ -37,7 +44,8 @@ class MatrixMessageAdapter implements IMessage {
public data: any, public data: any,
public source: Protocol, public source: Protocol,
public sender: IMessageTarget, public sender: IMessageTarget,
public target?: IMessageTarget) {} public target?: IMessageTarget
) {}
public get fullSenderID(): string { public get fullSenderID(): string {
return this.source.fullName + '/' + this.sender.id; return this.source.fullName + '/' + this.sender.id;
@ -70,23 +78,95 @@ class MatrixMessageAdapter implements IMessage {
} }
} }
/**
* File system utilities for storing data outside of the configuration.
*/
class MatrixFileSystemUtils {
constructor(private config: any) {}
get baseDir() {
return join(resolve('.matrix'), this.config.name);
}
get storageFile() {
return join(this.baseDir, 'db.json');
}
get cryptoStorageDir() {
return join(this.baseDir, 'sled');
}
get tokenFile() {
return join(this.baseDir, 'token');
}
public async ensureMatrixDirectory() {
try {
await fs.mkdir(resolve(this.baseDir), { recursive: true });
} catch {}
}
public async readAuth(): Promise<Record<string, string>> {
try {
await fs.stat(this.tokenFile);
} catch {
return {};
}
return JSON.parse(await fs.readFile(this.tokenFile, { encoding: 'utf-8' }));
}
public async writeAuth(data: Record<string, string>): Promise<void> {
await fs.writeFile(this.tokenFile, JSON.stringify(data), {
encoding: 'utf-8',
});
}
}
class MatrixProtocol extends Protocol { class MatrixProtocol extends Protocol {
public static Features = [
ProtocolFeatureFlag.COLORS,
ProtocolFeatureFlag.FORMATTING,
ProtocolFeatureFlag.PLAIN,
ProtocolFeatureFlag.HTML,
ProtocolFeatureFlag.EMOJI,
ProtocolFeatureFlag.IMAGES,
ProtocolFeatureFlag.VOICE,
ProtocolFeatureFlag.VIDEO,
ProtocolFeatureFlag.THREADS,
ProtocolFeatureFlag.REACTIONS,
ProtocolFeatureFlag.MENTION,
ProtocolFeatureFlag.OPTIONAL_ENCRYPTION,
ProtocolFeatureFlag.EVENT_MESSAGE,
ProtocolFeatureFlag.EVENT_ROOM_JOIN,
ProtocolFeatureFlag.EVENT_ROOM_LEAVE,
ProtocolFeatureFlag.KICK,
ProtocolFeatureFlag.BAN,
ProtocolFeatureFlag.MUTE,
];
public format: Formatter = new MatrixFormatter(); public format: Formatter = new MatrixFormatter();
public type = 'MatrixProtocol'; public type = 'MatrixProtocol';
public nameCache: Map<string, string> = new Map<string, string>(); public nameCache: Map<string, string> = new Map<string, string>();
private client?: MatrixClient; private client?: MatrixClient;
private fs = new MatrixFileSystemUtils(this.config);
public async handler(roomId: any, event: any): Promise<void> { public async handler(roomId: any, event: any): Promise<void> {
// Don't handle events that don't have contents (they were probably redacted) // Don't handle events that don't have contents (they were probably redacted)
if (!this.client || !this.me || !event.content) { return; } if (!this.client || !this.me || !event.content) {
return;
}
// Don't handle non-text events // Don't handle non-text events
if (event.content.msgtype !== 'm.text') { return; } if (event.content.msgtype !== 'm.text') {
return;
}
const msg = event.content.body; const msg = event.content.body;
// filter out events sent by the bot itself // filter out events sent by the bot itself
if (event.sender === this.me.id || !msg) { return; } if (event.sender === this.me.id || !msg) {
return;
}
const senderName = await this.getUserDisplayName(event.sender); const senderName = await this.getUserDisplayName(event.sender);
const roomName = await this.getRoomDisplayName(roomId); const roomName = await this.getRoomDisplayName(roomId);
@ -94,8 +174,8 @@ class MatrixProtocol extends Protocol {
EMessageType.message, EMessageType.message,
event, event,
this, this,
{ id: event.sender, name: senderName, }, { id: event.sender, name: senderName },
{ id: roomId, name: roomName, } { id: roomId, name: roomName }
); );
this.plugin.stream.emitTo('channel', 'message', newMessage); this.plugin.stream.emitTo('channel', 'message', newMessage);
} }
@ -116,7 +196,11 @@ class MatrixProtocol extends Protocol {
if (this.nameCache.has(id)) { if (this.nameCache.has(id)) {
return this.nameCache.get(id) as string; return this.nameCache.get(id) as string;
} }
const roomState = await this.client?.getRoomStateEvent(id, 'm.room.name', ''); const roomState = await this.client?.getRoomStateEvent(
id,
'm.room.name',
''
);
if (!roomState || !roomState.name) { if (!roomState || !roomState.name) {
return id; return id;
} }
@ -135,29 +219,101 @@ class MatrixProtocol extends Protocol {
this.me = { id, name }; this.me = { id, name };
} }
public start(...args: any[]): void { private async matrixLogin(
const stpath = `.matrix-${this.config.name}.db.json`; homeserverUrl: string,
const storage = new SimpleFsStorageProvider(stpath); username: string,
password: string,
deviceName: string
): Promise<{ accessToken: string; deviceId: string }> {
const body = {
type: 'm.login.password',
identifier: {
type: 'm.id.user',
user: username,
},
password: password,
initial_device_display_name: deviceName,
};
const response = await new MatrixClient(homeserverUrl, '').doRequest(
'POST',
'/_matrix/client/r0/login',
null,
body
);
const accessToken = response['access_token'];
const deviceId = response['device_id'];
if (!accessToken)
throw new Error('Expected access token in response - got nothing');
return { accessToken, deviceId };
}
public async start(...args: any[]): Promise<void> {
await this.fs.ensureMatrixDirectory();
this.client = new MatrixClient(this.config.homeserver, this.config.token, storage);
AutojoinRoomsMixin.setupOnClient(this.client);
LogService.setLevel(LogLevel.ERROR); LogService.setLevel(LogLevel.ERROR);
this.client.on('room.message', (...dargs) => const storage = new SimpleFsStorageProvider(this.fs.storageFile);
this.handler(dargs[0], dargs[1]) const cryptoStorage = new RustSdkCryptoStorageProvider(
.catch(e => logger.error(e))); this.fs.cryptoStorageDir,
StoreType.Sled
);
this.client.start() let { accessToken, deviceId } = await this.fs.readAuth();
.then(() => { if (!accessToken) {
// No token, log in
const loginInfo = await this.matrixLogin(
this.config.homeserver,
this.config.username,
this.config.password,
'Squeebot-Matrix'
);
accessToken = loginInfo.accessToken;
deviceId = loginInfo.deviceId;
await this.fs.writeAuth({ accessToken, deviceId });
await cryptoStorage.setDeviceId(deviceId);
}
this.client = new MatrixClient(
this.config.homeserver,
accessToken,
storage,
!!this.config.encrypted ? cryptoStorage : undefined
);
AutojoinRoomsMixin.setupOnClient(this.client);
this.client.on('room.message', (...dargs) =>
this.handler(dargs[0], dargs[1]).catch((e) => logger.error(e))
);
this.client.on(
'room.failed_decryption',
async (roomId: string, event: any, e: Error) =>
logger.error(
`Failed to decrypt ${roomId} ${event['event_id']} because `,
e
)
);
try {
await this.client.start();
this.running = true; this.running = true;
logger.info('[%s] Protocol "%s" ready', this.plugin.manifest.name, this.config.name); logger.info(
'[%s] Protocol "%s" ready',
this.plugin.manifest.name,
this.config.name
);
this.getSelf(); this.getSelf();
}) } catch (err) {
.catch((e) => {
this.stop(true); this.stop(true);
logger.error('[%s] Protocol "%s" failed to start', logger.error(
this.plugin.manifest.name, this.config.name); '[%s] Protocol "%s" failed to start',
}); this.plugin.manifest.name,
this.config.name,
(err as Error).message
);
}
} }
public stop(force = false): void { public stop(force = false): void {
@ -199,7 +355,11 @@ class MatrixProtocol extends Protocol {
try { try {
response = this.format.compose(data[0]); response = this.format.compose(data[0]);
} catch (e: any) { } catch (e: any) {
logger.error('[%s] Failed to compose message:', this.fullName, e.message); logger.error(
'[%s] Failed to compose message:',
this.fullName,
e.message
);
return false; return false;
} }
} }
@ -216,7 +376,7 @@ class MatrixProtocol extends Protocol {
} }
@InjectService(MatrixProtocol) @InjectService(MatrixProtocol)
@Configurable({instances: []}) @Configurable({ instances: [] })
class MatrixServicePlugin extends Plugin { class MatrixServicePlugin extends Plugin {
initialize(): void { initialize(): void {
const protoList = this.validateConfiguration(); const protoList = this.validateConfiguration();
@ -225,7 +385,7 @@ class MatrixServicePlugin extends Plugin {
private startAll(list: any[]): void { private startAll(list: any[]): void {
for (const ins of list) { for (const ins of list) {
const newProto = new MatrixProtocol(this, ins); const newProto = new MatrixProtocol(this, ins, MatrixProtocol.Features);
logger.log('[%s] Starting Matrix service "%s".', this.name, ins.name); logger.log('[%s] Starting Matrix service "%s".', this.name, ins.name);
this.monitor(newProto); this.monitor(newProto);
this.service?.use(newProto, true); this.service?.use(newProto, true);
@ -249,7 +409,7 @@ class MatrixServicePlugin extends Plugin {
continue; continue;
} }
if (!ins.name || !ins.token || !ins.homeserver) { if (!ins.name || !ins.username || !ins.password || !ins.homeserver) {
throw new Error('Invalid instance configuration!'); throw new Error('Invalid instance configuration!');
} }
@ -262,9 +422,11 @@ class MatrixServicePlugin extends Plugin {
@EventListener('pluginUnload') @EventListener('pluginUnload')
public unloadEventHandler(plugin: string | Plugin): void { public unloadEventHandler(plugin: string | Plugin): void {
if (plugin === this.name || plugin === this) { if (plugin === this.name || plugin === this) {
this.config.save().then(() => this.config
this.service?.stopAll().then(() => .save()
this.emit('pluginUnloaded', this))); .then(() =>
this.service?.stopAll().then(() => this.emit('pluginUnloaded', this))
);
} }
} }
} }

View File

@ -19,12 +19,20 @@
"type": "string", "type": "string",
"description": "Matrix homeserver URL" "description": "Matrix homeserver URL"
}, },
"token": { "username": {
"type": "string", "type": "string",
"description": "Matrix access token" "description": "Matrix username"
},
"password": {
"type": "string",
"description": "Matrix password"
},
"encrypted": {
"type": "boolean",
"description": "Use encryption"
} }
}, },
"required": ["name", "token", "homeserver"] "required": ["name", "username", "password", "homeserver"]
} }
} }
}, },

1984
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,8 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@squeebot/core": "^3.3.2", "@squeebot/core": "^3.6.0-1",
"matrix-bot-sdk": "^0.5.19", "matrix-bot-sdk": "^0.6.6",
"typescript": "^4.4.2" "typescript": "^5.1.6"
} }
} }

View File

@ -3,7 +3,7 @@
"plugins": [ "plugins": [
{ {
"name": "matrix", "name": "matrix",
"version": "1.0.3" "version": "1.2.0"
} }
], ],
"typescript": true "typescript": true