managarten/packages/matrix-bot-common/CLAUDE.md
Till-JS 43ac782892 docs(matrix-bot-common): document bot-to-bot loop prevention
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>
2026-02-14 14:05:34 +01:00

270 lines
7 KiB
Markdown

# @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:
```typescript
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-join
- `onModuleDestroy()` - Graceful shutdown
- `sendMessage(roomId, message)` - Send markdown message
- `sendReply(roomId, event, message)` - Reply to event
- `sendNotice(roomId, message)` - Non-highlighted message
- `downloadMedia(mxcUrl)` - Download from Matrix
- `uploadMedia(buffer, contentType, filename)` - Upload to Matrix
- `isBot(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:
```typescript
import { HealthController, createHealthProvider } from '@manacore/matrix-bot-common';
@Module({
controllers: [HealthController],
providers: [createHealthProvider('matrix-todo-bot')],
})
export class AppModule {}
```
Returns:
```json
{
"status": "ok",
"service": "matrix-todo-bot",
"timestamp": "2026-02-01T12:00:00.000Z",
"uptime": 3600
}
```
### MatrixMessageService
Injectable service for message operations:
```typescript
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:
```typescript
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
```typescript
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:
```typescript
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:
```typescript
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)
```typescript
// 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)
```typescript
import { markdownToHtml } from '@manacore/matrix-bot-common';
// Or extend BaseMatrixService which includes sendReply()
await this.sendReply(roomId, event, message);
```
## Installation
```bash
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
```bash
# Type check
pnpm --filter @manacore/matrix-bot-common type-check
# Add to a bot
pnpm --filter matrix-xxx-bot add @manacore/matrix-bot-common
```