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",
"description": "Matrix.org Service for Squeebot 3",
"tags": ["service", "matrix"],
"version": "1.0.3",
"version": "1.2.0",
"dependencies": ["control?"],
"npmDependencies": ["matrix-bot-sdk@0.5.19"]
"npmDependencies": ["matrix-bot-sdk@0.6.6"]
}

View File

@ -2,31 +2,38 @@ import {
Plugin,
Configurable,
EventListener,
InjectService
InjectService,
} from '@squeebot/core/lib/plugin';
import util from 'util';
import { promises as fs } from 'fs';
import {
MatrixClient,
AutojoinRoomsMixin,
RichReply,
SimpleFsStorageProvider,
LogService,
LogLevel,
RustSdkCryptoStorageProvider,
} 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 {
EMessageType,
Formatter,
IMessage,
IMessageTarget,
Protocol
Protocol,
ProtocolFeatureFlag,
} from '@squeebot/core/lib/types';
import { MatrixFormatter } from './format';
import { resolve, join } from 'path';
/**
* Message adapter for Matrix protocol.
*/
class MatrixMessageAdapter implements IMessage {
public time: Date = new Date();
public resolved = false;
@ -37,7 +44,8 @@ class MatrixMessageAdapter implements IMessage {
public data: any,
public source: Protocol,
public sender: IMessageTarget,
public target?: IMessageTarget) {}
public target?: IMessageTarget
) {}
public get fullSenderID(): string {
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 {
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 type = 'MatrixProtocol';
public nameCache: Map<string, string> = new Map<string, string>();
private client?: MatrixClient;
private fs = new MatrixFileSystemUtils(this.config);
public async handler(roomId: any, event: any): Promise<void> {
// 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
if (event.content.msgtype !== 'm.text') { return; }
if (event.content.msgtype !== 'm.text') {
return;
}
const msg = event.content.body;
// 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 roomName = await this.getRoomDisplayName(roomId);
@ -94,8 +174,8 @@ class MatrixProtocol extends Protocol {
EMessageType.message,
event,
this,
{ id: event.sender, name: senderName, },
{ id: roomId, name: roomName, }
{ id: event.sender, name: senderName },
{ id: roomId, name: roomName }
);
this.plugin.stream.emitTo('channel', 'message', newMessage);
}
@ -116,7 +196,11 @@ class MatrixProtocol extends Protocol {
if (this.nameCache.has(id)) {
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) {
return id;
}
@ -135,29 +219,101 @@ class MatrixProtocol extends Protocol {
this.me = { id, name };
}
public start(...args: any[]): void {
const stpath = `.matrix-${this.config.name}.db.json`;
const storage = new SimpleFsStorageProvider(stpath);
private async matrixLogin(
homeserverUrl: string,
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);
this.client.on('room.message', (...dargs) =>
this.handler(dargs[0], dargs[1])
.catch(e => logger.error(e)));
const storage = new SimpleFsStorageProvider(this.fs.storageFile);
const cryptoStorage = new RustSdkCryptoStorageProvider(
this.fs.cryptoStorageDir,
StoreType.Sled
);
this.client.start()
.then(() => {
this.running = true;
logger.info('[%s] Protocol "%s" ready', this.plugin.manifest.name, this.config.name);
this.getSelf();
})
.catch((e) => {
this.stop(true);
logger.error('[%s] Protocol "%s" failed to start',
this.plugin.manifest.name, this.config.name);
});
let { accessToken, deviceId } = await this.fs.readAuth();
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;
logger.info(
'[%s] Protocol "%s" ready',
this.plugin.manifest.name,
this.config.name
);
this.getSelf();
} catch (err) {
this.stop(true);
logger.error(
'[%s] Protocol "%s" failed to start',
this.plugin.manifest.name,
this.config.name,
(err as Error).message
);
}
}
public stop(force = false): void {
@ -199,7 +355,11 @@ class MatrixProtocol extends Protocol {
try {
response = this.format.compose(data[0]);
} 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;
}
}
@ -216,7 +376,7 @@ class MatrixProtocol extends Protocol {
}
@InjectService(MatrixProtocol)
@Configurable({instances: []})
@Configurable({ instances: [] })
class MatrixServicePlugin extends Plugin {
initialize(): void {
const protoList = this.validateConfiguration();
@ -225,7 +385,7 @@ class MatrixServicePlugin extends Plugin {
private startAll(list: any[]): void {
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);
this.monitor(newProto);
this.service?.use(newProto, true);
@ -249,7 +409,7 @@ class MatrixServicePlugin extends Plugin {
continue;
}
if (!ins.name || !ins.token || !ins.homeserver) {
if (!ins.name || !ins.username || !ins.password || !ins.homeserver) {
throw new Error('Invalid instance configuration!');
}
@ -262,9 +422,11 @@ class MatrixServicePlugin extends Plugin {
@EventListener('pluginUnload')
public unloadEventHandler(plugin: string | Plugin): void {
if (plugin === this.name || plugin === this) {
this.config.save().then(() =>
this.service?.stopAll().then(() =>
this.emit('pluginUnloaded', this)));
this.config
.save()
.then(() =>
this.service?.stopAll().then(() => this.emit('pluginUnloaded', this))
);
}
}
}

View File

@ -19,12 +19,20 @@
"type": "string",
"description": "Matrix homeserver URL"
},
"token": {
"username": {
"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": "",
"license": "ISC",
"dependencies": {
"@squeebot/core": "^3.3.2",
"matrix-bot-sdk": "^0.5.19",
"typescript": "^4.4.2"
"@squeebot/core": "^3.6.0-1",
"matrix-bot-sdk": "^0.6.6",
"typescript": "^5.1.6"
}
}

View File

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