optional encryption
This commit is contained in:
parent
d6ecd65a81
commit
11958b8db0
@ -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"]
|
||||
}
|
||||
|
236
matrix/plugin.ts
236
matrix/plugin.ts
@ -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))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
1984
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "matrix",
|
||||
"version": "1.0.3"
|
||||
"version": "1.2.0"
|
||||
}
|
||||
],
|
||||
"typescript": true
|
||||
|
Loading…
Reference in New Issue
Block a user