mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-28 21:37:43 +02:00
♻️ refactor: migrate all 18 Matrix bots to extend BaseMatrixService
All Matrix bots now extend BaseMatrixService from @manacore/matrix-bot-common: - matrix-calendar-bot - matrix-chat-bot - matrix-clock-bot - matrix-contacts-bot - matrix-mana-bot - matrix-manadeck-bot - matrix-nutriphi-bot - matrix-ollama-bot - matrix-picture-bot - matrix-planta-bot - matrix-presi-bot - matrix-project-doc-bot - matrix-questions-bot - matrix-skilltree-bot - matrix-storage-bot - matrix-todo-bot - matrix-tts-bot - matrix-zitare-bot Consolidated code: - Matrix client initialization (onModuleInit) - Graceful shutdown (onModuleDestroy) - sendMessage/sendReply/sendNotice methods - markdownToHtml conversion - Room permission checking - Media upload/download Estimated code reduction: ~1,500+ lines of duplicate code Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f4d8ed491c
commit
2567ea622c
18 changed files with 1472 additions and 2721 deletions
|
|
@ -1,132 +1,77 @@
|
|||
import { Injectable, Logger, OnModuleInit, OnModuleDestroy, Inject, forwardRef } from '@nestjs/common';
|
||||
import { Injectable, Inject, forwardRef } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
MatrixClient,
|
||||
SimpleFsStorageProvider,
|
||||
AutojoinRoomsMixin,
|
||||
RichReply,
|
||||
} from 'matrix-bot-sdk';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
BaseMatrixService,
|
||||
MatrixBotConfig,
|
||||
MatrixRoomEvent,
|
||||
} from '@manacore/matrix-bot-common';
|
||||
import { CommandRouterService, CommandContext } from './command-router.service';
|
||||
import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration';
|
||||
|
||||
@Injectable()
|
||||
export class MatrixService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(MatrixService.name);
|
||||
private client: MatrixClient;
|
||||
private botUserId: string = '';
|
||||
private readonly homeserverUrl: string;
|
||||
private readonly accessToken: string;
|
||||
private readonly allowedRooms: string[];
|
||||
private readonly storagePath: string;
|
||||
|
||||
export class MatrixService extends BaseMatrixService {
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
configService: ConfigService,
|
||||
@Inject(forwardRef(() => CommandRouterService))
|
||||
private commandRouter: CommandRouterService
|
||||
) {
|
||||
this.homeserverUrl = this.configService.get<string>('matrix.homeserverUrl', 'http://localhost:8008');
|
||||
this.accessToken = this.configService.get<string>('matrix.accessToken', '');
|
||||
this.allowedRooms = this.configService.get<string[]>('matrix.allowedRooms', []);
|
||||
this.storagePath = this.configService.get<string>('matrix.storagePath', './data/mana-bot-storage.json');
|
||||
super(configService);
|
||||
}
|
||||
|
||||
protected getConfig(): MatrixBotConfig {
|
||||
return {
|
||||
homeserverUrl: this.configService.get<string>('matrix.homeserverUrl') || 'http://localhost:8008',
|
||||
accessToken: this.configService.get<string>('matrix.accessToken') || '',
|
||||
storagePath: this.configService.get<string>('matrix.storagePath') || './data/mana-bot-storage.json',
|
||||
allowedRooms: this.configService.get<string[]>('matrix.allowedRooms') || [],
|
||||
};
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
if (!this.accessToken) {
|
||||
this.logger.warn('No Matrix access token configured. Bot will not start.');
|
||||
return;
|
||||
}
|
||||
await super.onModuleInit();
|
||||
|
||||
await this.initializeClient();
|
||||
}
|
||||
if (!this.client) return;
|
||||
|
||||
async onModuleDestroy() {
|
||||
if (this.client) {
|
||||
await this.client.stop();
|
||||
this.logger.log('Matrix client stopped');
|
||||
}
|
||||
}
|
||||
// Handle room invites with introduction
|
||||
this.client.on('room.invite', async (roomId: string) => {
|
||||
this.logger.log(`Invited to room ${roomId}, joining...`);
|
||||
await this.client.joinRoom(roomId);
|
||||
|
||||
private async initializeClient() {
|
||||
try {
|
||||
// Ensure storage directory exists
|
||||
const storageDir = path.dirname(this.storagePath);
|
||||
if (!fs.existsSync(storageDir)) {
|
||||
fs.mkdirSync(storageDir, { recursive: true });
|
||||
}
|
||||
|
||||
const storage = new SimpleFsStorageProvider(this.storagePath);
|
||||
this.client = new MatrixClient(this.homeserverUrl, this.accessToken, storage);
|
||||
|
||||
// Auto-join rooms when invited
|
||||
AutojoinRoomsMixin.setupOnClient(this.client);
|
||||
|
||||
// Handle room invites with introduction
|
||||
this.client.on('room.invite', async (roomId: string) => {
|
||||
this.logger.log(`Invited to room ${roomId}, joining...`);
|
||||
await this.client.joinRoom(roomId);
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await this.sendBotIntroduction(roomId);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to send introduction to ${roomId}:`, error);
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// Handle member joins for welcome message
|
||||
this.client.on('room.event', async (roomId: string, event: any) => {
|
||||
if (event.type === 'm.room.member' && event.content?.membership === 'join') {
|
||||
const userId = event.state_key;
|
||||
if (userId === this.botUserId) return;
|
||||
if (event.unsigned?.prev_content?.membership !== 'join') {
|
||||
await this.sendWelcomeMessage(roomId, userId);
|
||||
}
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await this.sendBotIntroduction(roomId);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to send introduction to ${roomId}:`, error);
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// Set up message handler
|
||||
this.client.on('room.message', async (roomId: string, event: any) => {
|
||||
await this.handleMessage(roomId, event);
|
||||
});
|
||||
|
||||
await this.client.start();
|
||||
this.botUserId = await this.client.getUserId();
|
||||
|
||||
this.logger.log(`Mana Gateway Bot connected to ${this.homeserverUrl}`);
|
||||
this.logger.log(`Bot user ID: ${this.botUserId}`);
|
||||
|
||||
if (this.allowedRooms.length > 0) {
|
||||
this.logger.log(`Allowed rooms: ${this.allowedRooms.join(', ')}`);
|
||||
} else {
|
||||
this.logger.log('No room restrictions - bot will respond in all rooms');
|
||||
// Handle member joins for welcome message
|
||||
this.client.on('room.event', async (roomId: string, event: any) => {
|
||||
if (event.type === 'm.room.member' && event.content?.membership === 'join') {
|
||||
const userId = event.state_key;
|
||||
if (userId === this.botUserId) return;
|
||||
if (event.unsigned?.prev_content?.membership !== 'join') {
|
||||
await this.sendWelcomeMessage(roomId, userId);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to initialize Matrix client:', error);
|
||||
}
|
||||
});
|
||||
|
||||
this.botUserId = await this.client.getUserId();
|
||||
this.logger.log(`Mana Gateway Bot connected`);
|
||||
this.logger.log(`Bot user ID: ${this.botUserId}`);
|
||||
}
|
||||
|
||||
private async handleMessage(roomId: string, event: any) {
|
||||
// Ignore messages from the bot itself
|
||||
if (event.sender === this.botUserId) return;
|
||||
|
||||
// Check if room is allowed
|
||||
if (this.allowedRooms.length > 0 && !this.allowedRooms.includes(roomId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const msgtype = event.content?.msgtype;
|
||||
const body = event.content?.body?.trim();
|
||||
|
||||
// Only handle text messages for now
|
||||
if (msgtype !== 'm.text' || !body) return;
|
||||
|
||||
protected async handleTextMessage(
|
||||
roomId: string,
|
||||
event: MatrixRoomEvent,
|
||||
message: string,
|
||||
sender: string
|
||||
): Promise<void> {
|
||||
const ctx: CommandContext = {
|
||||
roomId,
|
||||
userId: event.sender,
|
||||
message: body,
|
||||
userId: sender,
|
||||
message,
|
||||
event,
|
||||
};
|
||||
|
||||
|
|
@ -154,21 +99,6 @@ export class MatrixService implements OnModuleInit, OnModuleDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
async sendReply(roomId: string, event: any, message: string) {
|
||||
const reply = RichReply.createFor(roomId, event, message, this.markdownToHtml(message));
|
||||
reply.msgtype = 'm.text';
|
||||
await this.client.sendMessage(roomId, reply);
|
||||
}
|
||||
|
||||
async sendMessage(roomId: string, message: string) {
|
||||
await this.client.sendMessage(roomId, {
|
||||
msgtype: 'm.text',
|
||||
body: message,
|
||||
format: 'org.matrix.custom.html',
|
||||
formatted_body: this.markdownToHtml(message),
|
||||
});
|
||||
}
|
||||
|
||||
private async sendWelcomeMessage(roomId: string, userId: string) {
|
||||
try {
|
||||
await this.sendMessage(roomId, WELCOME_TEXT);
|
||||
|
|
@ -187,7 +117,7 @@ export class MatrixService implements OnModuleInit, OnModuleDestroy {
|
|||
msgtype: 'm.text',
|
||||
body: HELP_TEXT,
|
||||
format: 'org.matrix.custom.html',
|
||||
formatted_body: this.markdownToHtml(HELP_TEXT),
|
||||
formatted_body: this.markdownToHtmlPublic(HELP_TEXT),
|
||||
});
|
||||
|
||||
await this.client.sendStateEvent(roomId, 'm.room.pinned_events', '', {
|
||||
|
|
@ -199,7 +129,7 @@ export class MatrixService implements OnModuleInit, OnModuleDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
private markdownToHtml(text: string): string {
|
||||
private markdownToHtmlPublic(text: string): string {
|
||||
return text
|
||||
.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
|
|
@ -209,7 +139,7 @@ export class MatrixService implements OnModuleInit, OnModuleDestroy {
|
|||
.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
getClient(): MatrixClient {
|
||||
getClient() {
|
||||
return this.client;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue