mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
Added documentation for the isBot() method and automatic bot message filtering that prevents infinite loops when multiple bots share a room. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
7 KiB
7 KiB
@manacore/matrix-bot-common
Shared utilities and base classes for Matrix bots.
Purpose
This package consolidates common code patterns found across all 19 Matrix bots:
- ~4,000 lines of duplicate code reduced to shared utilities
- Consistent behavior across all bots
- Easier maintenance and updates
- Type-safe helpers for common patterns
Available Components
BaseMatrixService
Abstract base class that handles Matrix client lifecycle:
import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent } from '@manacore/matrix-bot-common';
@Injectable()
export class MyBotService extends BaseMatrixService {
constructor(configService: ConfigService) {
super(configService);
}
protected getConfig(): MatrixBotConfig {
return {
homeserverUrl: this.configService.get('matrix.homeserverUrl'),
accessToken: this.configService.get('matrix.accessToken'),
storagePath: this.configService.get('matrix.storagePath'),
allowedRooms: this.configService.get('matrix.allowedRooms') || [],
};
}
protected async handleTextMessage(
roomId: string,
event: MatrixRoomEvent,
message: string,
sender: string
) {
if (message === '!hello') {
await this.sendReply(roomId, event, 'Hello!');
}
}
// Optional: Handle voice messages
protected async handleAudioMessage(roomId: string, event: MatrixRoomEvent, sender: string) {
// Transcribe and process
}
// Optional: Send intro on room join
protected getIntroductionMessage(): string | null {
return 'Hello! I am a bot.';
}
}
Provides:
onModuleInit()- Client setup, storage, auto-joinonModuleDestroy()- Graceful shutdownsendMessage(roomId, message)- Send markdown messagesendReply(roomId, event, message)- Reply to eventsendNotice(roomId, message)- Non-highlighted messagedownloadMedia(mxcUrl)- Download from MatrixuploadMedia(buffer, contentType, filename)- Upload to MatrixisBot(sender)- Check if sender is a bot (prevents bot-to-bot loops)
Bot-to-Bot Loop Prevention:
The base class automatically ignores messages from other bots to prevent infinite message loops when multiple bots are in the same room. A sender is considered a bot if their Matrix ID localpart contains -bot or ends with bot (e.g., @mana-bot:server, @todobot:server).
HealthController
Shared health endpoint:
import { HealthController, createHealthProvider } from '@manacore/matrix-bot-common';
@Module({
controllers: [HealthController],
providers: [createHealthProvider('matrix-todo-bot')],
})
export class AppModule {}
Returns:
{
"status": "ok",
"service": "matrix-todo-bot",
"timestamp": "2026-02-01T12:00:00.000Z",
"uptime": 3600
}
MatrixMessageService
Injectable service for message operations:
import { MatrixMessageService } from '@manacore/matrix-bot-common';
@Injectable()
export class MyService {
constructor(private messageService: MatrixMessageService) {}
async doSomething(client: MatrixClient, roomId: string) {
await this.messageService.sendMessage(client, roomId, '**Bold** message');
await this.messageService.sendReaction(client, roomId, eventId, '👍');
await this.messageService.editMessage(client, roomId, eventId, 'Updated text');
}
}
KeywordCommandDetector
Natural language command detection:
import { KeywordCommandDetector, COMMON_KEYWORDS } from '@manacore/matrix-bot-common';
const detector = new KeywordCommandDetector([
...COMMON_KEYWORDS, // hilfe, help, status, etc.
{ keywords: ['liste', 'list', 'zeige'], command: 'list' },
{ keywords: ['neu', 'new', 'erstelle'], command: 'create' },
]);
const command = detector.detect('zeige mir alles'); // Returns 'list'
const command2 = detector.detect('random text'); // Returns null
Markdown Utilities
import { markdownToHtml, formatNumberedList, formatBulletList } from '@manacore/matrix-bot-common';
const html = markdownToHtml('**bold** and *italic*');
// '<strong>bold</strong> and <em>italic</em>'
const list = formatNumberedList(items, (item, i) => `${item.name} - ${item.status}`);
// '1. Item A - active\n2. Item B - done'
SessionHelper
Type-safe session data wrapper:
import { SessionHelper } from '@manacore/matrix-bot-common';
interface MySessionData {
currentItemId: string;
selectedModel: string;
itemList: string[];
}
const session = new SessionHelper<MySessionData>(sessionService, matrixUserId);
session.set('currentItemId', 'abc123');
const itemId = session.get('currentItemId'); // string | null
session.delete('currentItemId');
if (session.isLoggedIn()) {
const token = session.getToken();
}
UserListMapper
Number-based reference system:
import { UserListMapper } from '@manacore/matrix-bot-common';
const mapper = new UserListMapper<Contact>();
// After listing contacts to user
mapper.setList(userId, contacts);
// User says "!select 3"
const contact = mapper.getByNumber(userId, 3);
// For items with id field
import { UserIdListMapper } from '@manacore/matrix-bot-common';
const idMapper = new UserIdListMapper<{ id: string; name: string }>();
const itemId = idMapper.getIdByNumber(userId, 2);
Migration Guide
Before (duplicate code in each bot)
// matrix.service.ts - 50+ lines duplicated across 12 bots
private markdownToHtml(text: string): string {
return text
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
// ...
}
private 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);
}
After (using shared package)
import { markdownToHtml } from '@manacore/matrix-bot-common';
// Or extend BaseMatrixService which includes sendReply()
await this.sendReply(roomId, event, message);
Installation
pnpm --filter matrix-xxx-bot add @manacore/matrix-bot-common
File Structure
packages/matrix-bot-common/
├── src/
│ ├── index.ts # Main exports
│ ├── base/
│ │ ├── base-matrix.service.ts
│ │ ├── types.ts
│ │ └── index.ts
│ ├── health/
│ │ ├── health.controller.ts
│ │ └── index.ts
│ ├── message/
│ │ ├── message.service.ts
│ │ └── index.ts
│ ├── markdown/
│ │ ├── markdown-formatter.ts
│ │ └── index.ts
│ ├── keywords/
│ │ ├── keyword-detector.ts
│ │ └── index.ts
│ ├── session/
│ │ ├── session-helper.ts
│ │ └── index.ts
│ └── list-mapper/
│ ├── list-mapper.ts
│ └── index.ts
├── package.json
├── tsconfig.json
└── CLAUDE.md
Development
# Type check
pnpm --filter @manacore/matrix-bot-common type-check
# Add to a bot
pnpm --filter matrix-xxx-bot add @manacore/matrix-bot-common