mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-28 09:37:43 +02:00
Also restore @Global() decorator to I18nModule while keeping global: true in DynamicModule config for consistency with other modules like CreditModule. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
832 lines
24 KiB
TypeScript
832 lines
24 KiB
TypeScript
import { Injectable, Optional } from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import {
|
|
BaseMatrixService,
|
|
MatrixBotConfig,
|
|
MatrixRoomEvent,
|
|
UserListMapper,
|
|
KeywordCommandDetector,
|
|
COMMON_KEYWORDS,
|
|
handleCreditCommand,
|
|
type CreditCommandsHost,
|
|
} from '@manacore/matrix-bot-common';
|
|
import { PlantaService, Plant, PlantAnalysis } from '../planta/planta.service';
|
|
import {
|
|
SessionService,
|
|
TranscriptionService,
|
|
CreditService,
|
|
I18nService,
|
|
LOGIN_MESSAGES,
|
|
} from '@manacore/bot-services';
|
|
import { HELP_MESSAGE } from '../config/configuration';
|
|
|
|
@Injectable()
|
|
export class MatrixService extends BaseMatrixService implements CreditCommandsHost {
|
|
// Store last shown plants per user for reference by number
|
|
private plantsMapper = new UserListMapper<Plant>();
|
|
|
|
private readonly keywordDetector = new KeywordCommandDetector([
|
|
...COMMON_KEYWORDS,
|
|
{ keywords: ['pflanzen', 'plants', 'meine pflanzen', 'liste'], command: 'pflanzen' },
|
|
{ keywords: ['giessen', 'water', 'bewaessern', 'wasser geben'], command: 'giessen' },
|
|
{ keywords: ['faellig', 'due', 'anstehend', 'upcoming'], command: 'faellig' },
|
|
{ keywords: ['neu', 'new', 'neue pflanze', 'add'], command: 'neu' },
|
|
{ keywords: ['historie', 'history', 'verlauf', 'giess historie'], command: 'historie' },
|
|
{ keywords: ['intervall', 'interval', 'frequenz', 'wie oft'], command: 'intervall' },
|
|
{ keywords: ['credits', 'guthaben', 'kontostand'], command: 'credits' },
|
|
{ keywords: ['packages', 'pakete', 'preise'], command: 'packages' },
|
|
{ keywords: ['kaufen', 'buy'], command: 'buy' },
|
|
]);
|
|
|
|
// Field mappings for edit command
|
|
private readonly fieldMappings: Record<string, string> = {
|
|
name: 'name',
|
|
art: 'scientificName',
|
|
wissenschaftlich: 'scientificName',
|
|
scientific: 'scientificName',
|
|
licht: 'lightRequirements',
|
|
light: 'lightRequirements',
|
|
wasser: 'wateringFrequencyDays',
|
|
water: 'wateringFrequencyDays',
|
|
feuchtigkeit: 'humidity',
|
|
humidity: 'humidity',
|
|
temperatur: 'temperature',
|
|
temperature: 'temperature',
|
|
erde: 'soilType',
|
|
soil: 'soilType',
|
|
notizen: 'careNotes',
|
|
notes: 'careNotes',
|
|
};
|
|
|
|
// Expose services for credit commands mixin (CreditCommandsHost interface)
|
|
public sessionService: SessionService;
|
|
public creditService: CreditService;
|
|
public i18nService!: I18nService;
|
|
|
|
constructor(
|
|
configService: ConfigService,
|
|
private readonly transcriptionService: TranscriptionService,
|
|
private plantaService: PlantaService,
|
|
sessionService: SessionService,
|
|
creditService: CreditService,
|
|
@Optional() i18nService?: I18nService
|
|
) {
|
|
super(configService);
|
|
// Assign to public properties for credit commands mixin
|
|
this.sessionService = sessionService;
|
|
this.creditService = creditService;
|
|
if (i18nService) {
|
|
this.i18nService = i18nService;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// CreditCommandsHost interface implementation
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Send a credit message (delegates to protected sendMessage)
|
|
*/
|
|
async sendCreditMessage(roomId: string, message: string): Promise<void> {
|
|
await this.sendMessage(roomId, message);
|
|
}
|
|
|
|
/**
|
|
* Send a credit reply (delegates to protected sendReply)
|
|
*/
|
|
async sendCreditReply(roomId: string, event: MatrixRoomEvent, message: string): Promise<void> {
|
|
await this.sendReply(roomId, event, message);
|
|
}
|
|
|
|
protected override async handleAudioMessage(
|
|
roomId: string,
|
|
event: MatrixRoomEvent,
|
|
sender: string
|
|
): Promise<void> {
|
|
try {
|
|
const mxcUrl = event.content.url;
|
|
if (!mxcUrl) return;
|
|
|
|
const audioBuffer = await this.downloadMedia(mxcUrl);
|
|
const text = await this.transcriptionService.transcribe(audioBuffer);
|
|
if (!text) {
|
|
await this.sendMessage(roomId, '<p>❌ Sprachnachricht konnte nicht erkannt werden.</p>');
|
|
return;
|
|
}
|
|
|
|
await this.sendMessage(roomId, `<p>🎤 <em>"${text}"</em></p>`);
|
|
await this.handleTextMessage(roomId, event, text);
|
|
} catch (error) {
|
|
this.logger.error(`Audio transcription error: ${error}`);
|
|
await this.sendMessage(roomId, '<p>❌ Fehler bei der Spracherkennung.</p>');
|
|
}
|
|
}
|
|
|
|
protected override async handleImageMessage(
|
|
roomId: string,
|
|
event: MatrixRoomEvent,
|
|
sender: string
|
|
): Promise<void> {
|
|
try {
|
|
const mxcUrl = event.content.url;
|
|
if (!mxcUrl) return;
|
|
|
|
// Check auth
|
|
const token = await this.sessionService.getToken(sender);
|
|
if (!token) {
|
|
await this.sendMessage(
|
|
roomId,
|
|
'<p>🌱 Melde dich an, um Pflanzen zu analysieren: <code>!login email passwort</code></p>'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Send processing message
|
|
await this.sendMessage(roomId, '<p>🔍 Analysiere Pflanzenbild...</p>');
|
|
|
|
// Download image
|
|
const imageBuffer = await this.downloadMedia(mxcUrl);
|
|
const mimeType = (event.content.info?.mimetype as string) || 'image/jpeg';
|
|
const filename = event.content.body || 'plant.jpg';
|
|
|
|
// Upload and analyze
|
|
const result = await this.plantaService.uploadAndAnalyze(
|
|
token,
|
|
imageBuffer,
|
|
mimeType,
|
|
filename
|
|
);
|
|
|
|
if (result.error || !result.data) {
|
|
await this.sendMessage(roomId, `<p>❌ ${result.error || 'Analyse fehlgeschlagen'}</p>`);
|
|
return;
|
|
}
|
|
|
|
// Format and send result
|
|
const html = this.formatAnalysisResult(result.data);
|
|
await this.sendMessage(roomId, html);
|
|
} catch (error) {
|
|
this.logger.error(`Image analysis error: ${error}`);
|
|
await this.sendMessage(roomId, '<p>❌ Fehler bei der Bildanalyse.</p>');
|
|
}
|
|
}
|
|
|
|
private formatAnalysisResult(analysis: PlantAnalysis): string {
|
|
const confidence = analysis.confidence || 0;
|
|
const confidenceEmoji = confidence >= 80 ? '✅' : confidence >= 50 ? '🤔' : '❓';
|
|
|
|
let html = '<h3>🌿 Pflanze erkannt!</h3>';
|
|
|
|
// Identification
|
|
const scientificName = analysis.scientificName || analysis.identifiedSpecies || 'Unbekannt';
|
|
const commonNames = analysis.commonNames?.join(', ') || '';
|
|
|
|
html += `<p><strong>${scientificName}</strong>`;
|
|
if (commonNames) {
|
|
html += ` <em>(${commonNames})</em>`;
|
|
}
|
|
html += `<br/>${confidenceEmoji} Konfidenz: ${confidence}%</p>`;
|
|
|
|
// Health
|
|
if (analysis.healthAssessment) {
|
|
const healthEmoji = this.getHealthStatusEmoji(analysis.healthAssessment);
|
|
html += `<p><strong>Gesundheit:</strong> ${healthEmoji} ${this.translateHealthStatus(analysis.healthAssessment)}`;
|
|
if (analysis.healthDetails) {
|
|
html += `<br/><em>${analysis.healthDetails}</em>`;
|
|
}
|
|
html += '</p>';
|
|
|
|
if (analysis.issues && analysis.issues.length > 0) {
|
|
html += '<p><strong>Probleme:</strong></p><ul>';
|
|
for (const issue of analysis.issues) {
|
|
html += `<li>⚠️ ${issue}</li>`;
|
|
}
|
|
html += '</ul>';
|
|
}
|
|
}
|
|
|
|
// Care tips
|
|
html += '<p><strong>📋 Pflegetipps:</strong></p><ul>';
|
|
if (analysis.lightAdvice) {
|
|
html += `<li>☀️ ${analysis.lightAdvice}</li>`;
|
|
}
|
|
if (analysis.wateringAdvice) {
|
|
html += `<li>💧 ${analysis.wateringAdvice}</li>`;
|
|
}
|
|
if (analysis.generalTips && analysis.generalTips.length > 0) {
|
|
for (const tip of analysis.generalTips.slice(0, 3)) {
|
|
html += `<li>🌱 ${tip}</li>`;
|
|
}
|
|
}
|
|
html += '</ul>';
|
|
|
|
// Call to action
|
|
html += '<p><em>Pflanze hinzufuegen mit: <code>!neu Pflanzenname</code></em></p>';
|
|
|
|
return html;
|
|
}
|
|
|
|
private getHealthStatusEmoji(status: string): string {
|
|
const emojiMap: Record<string, string> = {
|
|
healthy: '💚',
|
|
minor_issues: '💛',
|
|
needs_care: '🧡',
|
|
critical: '❤️',
|
|
};
|
|
return emojiMap[status] || '💚';
|
|
}
|
|
|
|
private translateHealthStatus(status: string): string {
|
|
const statusMap: Record<string, string> = {
|
|
healthy: 'Gesund',
|
|
minor_issues: 'Kleinere Probleme',
|
|
needs_care: 'Braucht Pflege',
|
|
critical: 'Kritisch',
|
|
};
|
|
return statusMap[status] || status;
|
|
}
|
|
|
|
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/bot-storage.json',
|
|
allowedRooms: this.configService.get<string[]>('matrix.allowedRooms') || [],
|
|
};
|
|
}
|
|
|
|
protected async handleTextMessage(
|
|
roomId: string,
|
|
event: MatrixRoomEvent,
|
|
body: string
|
|
): Promise<void> {
|
|
this.logger.debug(`[PLANTA] handleTextMessage called: body="${body}", sender=${event.sender}`);
|
|
|
|
// Check for keyword commands first
|
|
const keywordCommand = this.keywordDetector.detect(body);
|
|
if (keywordCommand) {
|
|
body = `!${keywordCommand}`;
|
|
}
|
|
|
|
if (!body.startsWith('!')) {
|
|
this.logger.debug(`[PLANTA] Message doesn't start with ! - ignoring`);
|
|
return;
|
|
}
|
|
|
|
const sender = event.sender;
|
|
const parts = body.slice(1).split(/\s+/);
|
|
const command = parts[0].toLowerCase();
|
|
const args = parts.slice(1);
|
|
const argString = args.join(' ');
|
|
this.logger.debug(`[PLANTA] Processing command: ${command}`);
|
|
|
|
try {
|
|
// Handle credit commands first (credits, packages, buy)
|
|
if (await handleCreditCommand(this, roomId, event, sender, command, argString)) {
|
|
return;
|
|
}
|
|
|
|
switch (command) {
|
|
case 'help':
|
|
case 'hilfe':
|
|
this.logger.debug(`[PLANTA] Sending help message to room ${roomId}`);
|
|
await this.sendMessage(roomId, HELP_MESSAGE);
|
|
this.logger.debug(`[PLANTA] Help message sent successfully`);
|
|
break;
|
|
|
|
case 'status':
|
|
await this.handleStatus(roomId, sender);
|
|
break;
|
|
|
|
case 'pflanzen':
|
|
case 'plants':
|
|
case 'liste':
|
|
await this.handleListPlants(roomId, sender);
|
|
break;
|
|
|
|
case 'pflanze':
|
|
case 'plant':
|
|
case 'details':
|
|
await this.handlePlantDetails(roomId, sender, args[0]);
|
|
break;
|
|
|
|
case 'neu':
|
|
case 'new':
|
|
case 'add':
|
|
await this.handleAddPlant(roomId, sender, argString);
|
|
break;
|
|
|
|
case 'loeschen':
|
|
case 'delete':
|
|
case 'entfernen':
|
|
await this.handleDeletePlant(roomId, sender, args[0]);
|
|
break;
|
|
|
|
case 'edit':
|
|
case 'bearbeiten':
|
|
await this.handleEditPlant(roomId, sender, args);
|
|
break;
|
|
|
|
case 'giessen':
|
|
case 'water':
|
|
await this.handleWaterPlant(roomId, sender, args[0], args.slice(1).join(' '));
|
|
break;
|
|
|
|
case 'faellig':
|
|
case 'due':
|
|
case 'upcoming':
|
|
await this.handleUpcomingWaterings(roomId, sender);
|
|
break;
|
|
|
|
case 'historie':
|
|
case 'history':
|
|
case 'verlauf':
|
|
await this.handleWateringHistory(roomId, sender, args[0]);
|
|
break;
|
|
|
|
case 'intervall':
|
|
case 'interval':
|
|
case 'frequenz':
|
|
await this.handleSetInterval(roomId, sender, args[0], args[1]);
|
|
break;
|
|
|
|
default:
|
|
await this.sendMessage(
|
|
roomId,
|
|
`<p>Unbekannter Befehl: <code>${command}</code>. Nutze <code>!help</code> fuer Hilfe.</p>`
|
|
);
|
|
}
|
|
} catch (error) {
|
|
this.logger.error(`Error handling command ${command}:`, error);
|
|
await this.sendMessage(roomId, `<p>Fehler: ${(error as Error).message}</p>`);
|
|
}
|
|
}
|
|
|
|
private async requireAuth(sender: string): Promise<string> {
|
|
const token = await this.sessionService.getToken(sender);
|
|
if (!token) {
|
|
throw new Error(LOGIN_MESSAGES.planta);
|
|
}
|
|
return token;
|
|
}
|
|
|
|
private async handleStatus(roomId: string, sender: string) {
|
|
const backendOk = await this.plantaService.checkHealth();
|
|
const loggedIn = await this.sessionService.isLoggedIn(sender);
|
|
const sessions = await this.sessionService.getSessionCount();
|
|
const session = await this.sessionService.getSession(sender);
|
|
const token = await this.sessionService.getToken(sender);
|
|
|
|
let statusHtml = `<h3>Planta Bot Status</h3><ul>`;
|
|
statusHtml += `<li>Backend: ${backendOk ? '✅ Online' : '❌ Offline'}</li>`;
|
|
statusHtml += `<li>Aktive Sessions: ${sessions}</li>`;
|
|
|
|
if (loggedIn && session && token) {
|
|
const balance = await this.creditService.getBalance(token);
|
|
statusHtml += `<li>👤 Angemeldet als: ${session.email}</li>`;
|
|
statusHtml += `<li>⚡ Credits: ${balance.balance.toFixed(2)}</li>`;
|
|
} else {
|
|
statusHtml += `<li>👤 Nicht angemeldet</li>`;
|
|
statusHtml += `<li>💡 Login: <code>!login email passwort</code></li>`;
|
|
}
|
|
statusHtml += `</ul>`;
|
|
|
|
await this.sendMessage(roomId, statusHtml);
|
|
}
|
|
|
|
// Plant handlers
|
|
private async handleListPlants(roomId: string, sender: string) {
|
|
const token = await this.requireAuth(sender);
|
|
const result = await this.plantaService.getPlants(token);
|
|
|
|
if (result.error) {
|
|
await this.sendMessage(roomId, `<p>Fehler: ${result.error}</p>`);
|
|
return;
|
|
}
|
|
|
|
const plants = result.data || [];
|
|
this.plantsMapper.setList(sender, plants);
|
|
|
|
if (plants.length === 0) {
|
|
await this.sendMessage(
|
|
roomId,
|
|
'<p>Keine Pflanzen vorhanden. Fuege eine mit <code>!neu Name</code> hinzu.</p>'
|
|
);
|
|
return;
|
|
}
|
|
|
|
let html = '<h3>Deine Pflanzen</h3><ol>';
|
|
for (const plant of plants) {
|
|
const scientific = plant.scientificName ? ` <em>(${plant.scientificName})</em>` : '';
|
|
const health = this.getHealthEmoji(plant.healthStatus);
|
|
html += `<li>${health} <strong>${plant.name}</strong>${scientific}</li>`;
|
|
}
|
|
html += '</ol>';
|
|
html +=
|
|
'<p><em>Nutze <code>!pflanze [nr]</code> fuer Details oder <code>!faellig</code> fuer Giess-Status</em></p>';
|
|
|
|
await this.sendMessage(roomId, html);
|
|
}
|
|
|
|
private async handlePlantDetails(roomId: string, sender: string, numberStr: string) {
|
|
const token = await this.requireAuth(sender);
|
|
const plant = this.getPlantByNumber(sender, numberStr);
|
|
|
|
if (!plant) {
|
|
await this.sendMessage(
|
|
roomId,
|
|
'<p>Ungueltige Nummer. Nutze zuerst <code>!pflanzen</code></p>'
|
|
);
|
|
return;
|
|
}
|
|
|
|
const result = await this.plantaService.getPlant(token, plant.id);
|
|
if (result.error) {
|
|
await this.sendMessage(roomId, `<p>Fehler: ${result.error}</p>`);
|
|
return;
|
|
}
|
|
|
|
const p = result.data!;
|
|
const health = this.getHealthEmoji(p.healthStatus);
|
|
let html = `<h3>${health} ${p.name}</h3>`;
|
|
|
|
if (p.scientificName) html += `<p><em>${p.scientificName}</em></p>`;
|
|
|
|
html += '<ul>';
|
|
if (p.lightRequirements) html += `<li>Licht: ${this.translateLight(p.lightRequirements)}</li>`;
|
|
if (p.wateringFrequencyDays) html += `<li>Giessen: alle ${p.wateringFrequencyDays} Tage</li>`;
|
|
if (p.humidity) html += `<li>Feuchtigkeit: ${this.translateHumidity(p.humidity)}</li>`;
|
|
if (p.temperature) html += `<li>Temperatur: ${p.temperature}</li>`;
|
|
if (p.soilType) html += `<li>Erde: ${p.soilType}</li>`;
|
|
if (p.healthStatus) html += `<li>Gesundheit: ${this.translateHealth(p.healthStatus)}</li>`;
|
|
if (p.acquiredAt)
|
|
html += `<li>Erworben: ${new Date(p.acquiredAt).toLocaleDateString('de-DE')}</li>`;
|
|
html += '</ul>';
|
|
|
|
if (p.careNotes) {
|
|
html += `<p><strong>Notizen:</strong> ${p.careNotes}</p>`;
|
|
}
|
|
|
|
await this.sendMessage(roomId, html);
|
|
}
|
|
|
|
private async handleAddPlant(roomId: string, sender: string, name: string) {
|
|
if (!name) {
|
|
await this.sendMessage(roomId, '<p>Verwendung: <code>!neu Pflanzenname</code></p>');
|
|
return;
|
|
}
|
|
|
|
const token = await this.requireAuth(sender);
|
|
const result = await this.plantaService.createPlant(token, name);
|
|
|
|
if (result.error) {
|
|
await this.sendMessage(roomId, `<p>Fehler: ${result.error}</p>`);
|
|
return;
|
|
}
|
|
|
|
// Clear cached list
|
|
this.plantsMapper.clearList(sender);
|
|
await this.sendMessage(
|
|
roomId,
|
|
`<p>Pflanze <strong>${result.data!.name}</strong> hinzugefuegt!</p>
|
|
<p><em>Nutze <code>!edit</code> um Details wie Licht, Wasser etc. zu setzen.</em></p>`
|
|
);
|
|
}
|
|
|
|
private async handleDeletePlant(roomId: string, sender: string, numberStr: string) {
|
|
const token = await this.requireAuth(sender);
|
|
const plant = this.getPlantByNumber(sender, numberStr);
|
|
|
|
if (!plant) {
|
|
await this.sendMessage(
|
|
roomId,
|
|
'<p>Ungueltige Nummer. Nutze zuerst <code>!pflanzen</code></p>'
|
|
);
|
|
return;
|
|
}
|
|
|
|
const result = await this.plantaService.deletePlant(token, plant.id);
|
|
|
|
if (result.error) {
|
|
await this.sendMessage(roomId, `<p>Fehler: ${result.error}</p>`);
|
|
return;
|
|
}
|
|
|
|
// Clear cached list
|
|
this.plantsMapper.clearList(sender);
|
|
await this.sendMessage(roomId, `<p>Pflanze <strong>${plant.name}</strong> entfernt.</p>`);
|
|
}
|
|
|
|
private async handleEditPlant(roomId: string, sender: string, args: string[]) {
|
|
if (args.length < 3) {
|
|
await this.sendMessage(
|
|
roomId,
|
|
'<p>Verwendung: <code>!edit [nr] [feld] [wert]</code></p><p>Felder: name, art, licht, wasser, notizen</p>'
|
|
);
|
|
return;
|
|
}
|
|
|
|
const token = await this.requireAuth(sender);
|
|
const plant = this.getPlantByNumber(sender, args[0]);
|
|
|
|
if (!plant) {
|
|
await this.sendMessage(
|
|
roomId,
|
|
'<p>Ungueltige Nummer. Nutze zuerst <code>!pflanzen</code></p>'
|
|
);
|
|
return;
|
|
}
|
|
|
|
const fieldInput = args[1].toLowerCase();
|
|
const field = this.fieldMappings[fieldInput];
|
|
const value = args.slice(2).join(' ');
|
|
|
|
if (!field) {
|
|
await this.sendMessage(
|
|
roomId,
|
|
`<p>Unbekanntes Feld: <code>${fieldInput}</code></p><p>Verfuegbar: name, art, licht, wasser, notizen</p>`
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Validate and convert values
|
|
let updateValue: string | number = value;
|
|
if (field === 'wateringFrequencyDays') {
|
|
updateValue = parseInt(value, 10);
|
|
if (isNaN(updateValue) || updateValue < 1) {
|
|
await this.sendMessage(roomId, '<p>Wasser-Intervall muss eine positive Zahl sein.</p>');
|
|
return;
|
|
}
|
|
} else if (field === 'lightRequirements') {
|
|
const lightMap: Record<string, string> = {
|
|
wenig: 'low',
|
|
low: 'low',
|
|
gering: 'low',
|
|
mittel: 'medium',
|
|
medium: 'medium',
|
|
hell: 'bright',
|
|
bright: 'bright',
|
|
viel: 'bright',
|
|
direkt: 'direct',
|
|
direct: 'direct',
|
|
sonne: 'direct',
|
|
};
|
|
updateValue = lightMap[value.toLowerCase()];
|
|
if (!updateValue) {
|
|
await this.sendMessage(
|
|
roomId,
|
|
'<p>Licht-Werte: wenig/low, mittel/medium, hell/bright, direkt/direct</p>'
|
|
);
|
|
return;
|
|
}
|
|
} else if (field === 'humidity') {
|
|
const humidityMap: Record<string, string> = {
|
|
niedrig: 'low',
|
|
low: 'low',
|
|
gering: 'low',
|
|
trocken: 'low',
|
|
mittel: 'medium',
|
|
medium: 'medium',
|
|
normal: 'medium',
|
|
hoch: 'high',
|
|
high: 'high',
|
|
feucht: 'high',
|
|
};
|
|
updateValue = humidityMap[value.toLowerCase()];
|
|
if (!updateValue) {
|
|
await this.sendMessage(
|
|
roomId,
|
|
'<p>Feuchtigkeits-Werte: niedrig/low, mittel/medium, hoch/high</p>'
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const result = await this.plantaService.updatePlant(token, plant.id, {
|
|
[field]: updateValue,
|
|
});
|
|
|
|
if (result.error) {
|
|
await this.sendMessage(roomId, `<p>Fehler: ${result.error}</p>`);
|
|
return;
|
|
}
|
|
|
|
await this.sendMessage(
|
|
roomId,
|
|
`<p><strong>${plant.name}</strong>: ${fieldInput} aktualisiert.</p>`
|
|
);
|
|
}
|
|
|
|
// Watering handlers
|
|
private async handleWaterPlant(
|
|
roomId: string,
|
|
sender: string,
|
|
numberStr: string,
|
|
notes?: string
|
|
) {
|
|
const token = await this.requireAuth(sender);
|
|
const plant = this.getPlantByNumber(sender, numberStr);
|
|
|
|
if (!plant) {
|
|
await this.sendMessage(
|
|
roomId,
|
|
'<p>Ungueltige Nummer. Nutze zuerst <code>!pflanzen</code></p>'
|
|
);
|
|
return;
|
|
}
|
|
|
|
const result = await this.plantaService.waterPlant(token, plant.id, notes || undefined);
|
|
|
|
if (result.error) {
|
|
await this.sendMessage(roomId, `<p>Fehler: ${result.error}</p>`);
|
|
return;
|
|
}
|
|
|
|
let html = `<p><strong>${plant.name}</strong> gegossen!</p>`;
|
|
if (notes) {
|
|
html += `<p><em>Notiz: ${notes}</em></p>`;
|
|
}
|
|
|
|
await this.sendMessage(roomId, html);
|
|
}
|
|
|
|
private async handleUpcomingWaterings(roomId: string, sender: string) {
|
|
const token = await this.requireAuth(sender);
|
|
const result = await this.plantaService.getUpcomingWaterings(token);
|
|
|
|
if (result.error) {
|
|
await this.sendMessage(roomId, `<p>Fehler: ${result.error}</p>`);
|
|
return;
|
|
}
|
|
|
|
const upcoming = result.data || [];
|
|
|
|
if (upcoming.length === 0) {
|
|
await this.sendMessage(
|
|
roomId,
|
|
'<p>Keine Pflanzen muessen in den naechsten Tagen gegossen werden.</p>'
|
|
);
|
|
return;
|
|
}
|
|
|
|
let html = '<h3>Giess-Status</h3><ul>';
|
|
for (const item of upcoming) {
|
|
const status = item.isOverdue
|
|
? `<strong style="color: red;">Ueberfaellig (${Math.abs(item.daysUntilWatering)} Tage)</strong>`
|
|
: item.daysUntilWatering === 0
|
|
? '<strong style="color: orange;">Heute</strong>'
|
|
: `in ${item.daysUntilWatering} Tag${item.daysUntilWatering > 1 ? 'en' : ''}`;
|
|
html += `<li><strong>${item.plant.name}</strong>: ${status}</li>`;
|
|
}
|
|
html += '</ul>';
|
|
|
|
// Store plants for reference
|
|
this.plantsMapper.setList(
|
|
sender,
|
|
upcoming.map((u) => u.plant)
|
|
);
|
|
|
|
await this.sendMessage(roomId, html);
|
|
}
|
|
|
|
private async handleWateringHistory(roomId: string, sender: string, numberStr: string) {
|
|
const token = await this.requireAuth(sender);
|
|
const plant = this.getPlantByNumber(sender, numberStr);
|
|
|
|
if (!plant) {
|
|
await this.sendMessage(
|
|
roomId,
|
|
'<p>Ungueltige Nummer. Nutze zuerst <code>!pflanzen</code></p>'
|
|
);
|
|
return;
|
|
}
|
|
|
|
const result = await this.plantaService.getWateringHistory(token, plant.id);
|
|
|
|
if (result.error) {
|
|
await this.sendMessage(roomId, `<p>Fehler: ${result.error}</p>`);
|
|
return;
|
|
}
|
|
|
|
const logs = result.data || [];
|
|
|
|
if (logs.length === 0) {
|
|
await this.sendMessage(
|
|
roomId,
|
|
`<p><strong>${plant.name}</strong> wurde noch nie gegossen.</p>`
|
|
);
|
|
return;
|
|
}
|
|
|
|
let html = `<h3>Giess-Historie: ${plant.name}</h3><ul>`;
|
|
for (const log of logs.slice(0, 10)) {
|
|
const date = new Date(log.wateredAt).toLocaleDateString('de-DE', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
const notes = log.notes ? ` - ${log.notes}` : '';
|
|
html += `<li>${date}${notes}</li>`;
|
|
}
|
|
html += '</ul>';
|
|
|
|
if (logs.length > 10) {
|
|
html += `<p><em>...und ${logs.length - 10} weitere Eintraege</em></p>`;
|
|
}
|
|
|
|
await this.sendMessage(roomId, html);
|
|
}
|
|
|
|
private async handleSetInterval(
|
|
roomId: string,
|
|
sender: string,
|
|
numberStr: string,
|
|
daysStr: string
|
|
) {
|
|
if (!numberStr || !daysStr) {
|
|
await this.sendMessage(roomId, '<p>Verwendung: <code>!intervall [nr] [tage]</code></p>');
|
|
return;
|
|
}
|
|
|
|
const token = await this.requireAuth(sender);
|
|
const plant = this.getPlantByNumber(sender, numberStr);
|
|
|
|
if (!plant) {
|
|
await this.sendMessage(
|
|
roomId,
|
|
'<p>Ungueltige Nummer. Nutze zuerst <code>!pflanzen</code></p>'
|
|
);
|
|
return;
|
|
}
|
|
|
|
const days = parseInt(daysStr, 10);
|
|
if (isNaN(days) || days < 1) {
|
|
await this.sendMessage(roomId, '<p>Tage muss eine positive Zahl sein.</p>');
|
|
return;
|
|
}
|
|
|
|
const result = await this.plantaService.updateWateringSchedule(token, plant.id, days);
|
|
|
|
if (result.error) {
|
|
await this.sendMessage(roomId, `<p>Fehler: ${result.error}</p>`);
|
|
return;
|
|
}
|
|
|
|
await this.sendMessage(
|
|
roomId,
|
|
`<p>Giess-Intervall fuer <strong>${plant.name}</strong> auf ${days} Tage gesetzt.</p>`
|
|
);
|
|
}
|
|
|
|
// Helper methods
|
|
private getPlantByNumber(sender: string, numberStr: string): Plant | null {
|
|
const num = parseInt(numberStr, 10);
|
|
if (isNaN(num)) return null;
|
|
return this.plantsMapper.getByNumber(sender, num);
|
|
}
|
|
|
|
private getHealthEmoji(status?: string): string {
|
|
switch (status) {
|
|
case 'healthy':
|
|
return '🌱'; // Seedling
|
|
case 'needs_attention':
|
|
return '⚠️'; // Warning
|
|
case 'sick':
|
|
return '🤢'; // Wilted
|
|
default:
|
|
return '🌱';
|
|
}
|
|
}
|
|
|
|
private translateLight(light: string): string {
|
|
const map: Record<string, string> = {
|
|
low: 'Wenig Licht',
|
|
medium: 'Mittleres Licht',
|
|
bright: 'Helles Licht',
|
|
direct: 'Direktes Sonnenlicht',
|
|
};
|
|
return map[light] || light;
|
|
}
|
|
|
|
private translateHumidity(humidity: string): string {
|
|
const map: Record<string, string> = {
|
|
low: 'Niedrig',
|
|
medium: 'Mittel',
|
|
high: 'Hoch',
|
|
};
|
|
return map[humidity] || humidity;
|
|
}
|
|
|
|
private translateHealth(health: string): string {
|
|
const map: Record<string, string> = {
|
|
healthy: 'Gesund',
|
|
needs_attention: 'Braucht Aufmerksamkeit',
|
|
sick: 'Krank',
|
|
};
|
|
return map[health] || health;
|
|
}
|
|
}
|