mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-21 00:26:42 +02:00
feat(packages): add @manacore/bot-services shared package
Introduces a new shared package containing transport-agnostic business logic services for Matrix bots and the Gateway. This enables: - Individual bots to import shared services - A unified Gateway bot to combine all features - Code reuse without duplication Services included: - TodoService: Task management with projects, priorities, dates - CalendarService: Events, calendars, reminders - AiService: Ollama LLM integration, chat sessions, vision - ClockService: Timers, alarms, world clocks (API client) - Placeholder modules for Nutrition, Quotes, Stats, Docs Key features: - Pluggable storage providers (file-based, in-memory, custom) - German natural language input parsing - NestJS module system with dependency injection - Fully testable in isolation https://claude.ai/code/session_015bwcqVRiFmSydYTjvDJGTc
This commit is contained in:
parent
bea066c7f8
commit
68a6c7a8d6
28 changed files with 2492 additions and 0 deletions
8
packages/bot-services/src/shared/index.ts
Normal file
8
packages/bot-services/src/shared/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Shared types
|
||||
export * from './types';
|
||||
|
||||
// Storage providers
|
||||
export { FileStorageProvider, MemoryStorageProvider } from './storage';
|
||||
|
||||
// Utility functions
|
||||
export * from './utils';
|
||||
71
packages/bot-services/src/shared/storage.ts
Normal file
71
packages/bot-services/src/shared/storage.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { Logger } from '@nestjs/common';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { StorageProvider } from './types';
|
||||
|
||||
/**
|
||||
* File-based JSON storage provider
|
||||
* Used for local GDPR-compliant data storage
|
||||
*/
|
||||
export class FileStorageProvider<T> implements StorageProvider<T> {
|
||||
private readonly logger = new Logger(FileStorageProvider.name);
|
||||
private readonly filePath: string;
|
||||
private readonly defaultData: T;
|
||||
|
||||
constructor(filePath: string, defaultData: T) {
|
||||
this.filePath = filePath;
|
||||
this.defaultData = defaultData;
|
||||
}
|
||||
|
||||
async load(): Promise<T> {
|
||||
try {
|
||||
const dir = path.dirname(this.filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
if (fs.existsSync(this.filePath)) {
|
||||
const content = fs.readFileSync(this.filePath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
} else {
|
||||
await this.save(this.defaultData);
|
||||
return this.defaultData;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to load data from ${this.filePath}:`, error);
|
||||
return this.defaultData;
|
||||
}
|
||||
}
|
||||
|
||||
async save(data: T): Promise<void> {
|
||||
try {
|
||||
const dir = path.dirname(this.filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to save data to ${this.filePath}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory storage provider (for testing)
|
||||
*/
|
||||
export class MemoryStorageProvider<T> implements StorageProvider<T> {
|
||||
private data: T;
|
||||
|
||||
constructor(defaultData: T) {
|
||||
this.data = defaultData;
|
||||
}
|
||||
|
||||
async load(): Promise<T> {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
async save(data: T): Promise<void> {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
66
packages/bot-services/src/shared/types.ts
Normal file
66
packages/bot-services/src/shared/types.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* Common types used across all bot services
|
||||
*/
|
||||
|
||||
// Base entity interface
|
||||
export interface BaseEntity {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
// User-scoped entity
|
||||
export interface UserEntity extends BaseEntity {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
// Storage provider interface - allows swapping file/db storage
|
||||
export interface StorageProvider<T> {
|
||||
load(): Promise<T>;
|
||||
save(data: T): Promise<void>;
|
||||
}
|
||||
|
||||
// Service configuration
|
||||
export interface ServiceConfig {
|
||||
storagePath?: string;
|
||||
apiUrl?: string;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
// Result type for operations
|
||||
export type Result<T, E = Error> = { success: true; data: T } | { success: false; error: E };
|
||||
|
||||
// Pagination
|
||||
export interface PaginationOptions {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface PaginatedResult<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
// Date range filter
|
||||
export interface DateRange {
|
||||
start: Date;
|
||||
end: Date;
|
||||
}
|
||||
|
||||
// Priority levels
|
||||
export type Priority = 'low' | 'medium' | 'high' | 'urgent';
|
||||
export const PRIORITY_VALUES: Record<Priority, number> = {
|
||||
urgent: 1,
|
||||
high: 2,
|
||||
medium: 3,
|
||||
low: 4,
|
||||
};
|
||||
|
||||
// Common stats interface
|
||||
export interface ServiceStats {
|
||||
total: number;
|
||||
active: number;
|
||||
completed?: number;
|
||||
}
|
||||
110
packages/bot-services/src/shared/utils.ts
Normal file
110
packages/bot-services/src/shared/utils.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* Utility functions for bot services
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generate a unique ID
|
||||
*/
|
||||
export function generateId(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ISO date string for today
|
||||
*/
|
||||
export function getTodayISO(): string {
|
||||
return new Date().toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date at start of day
|
||||
*/
|
||||
export function startOfDay(date: Date = new Date()): Date {
|
||||
const result = new Date(date);
|
||||
result.setHours(0, 0, 0, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date at end of day
|
||||
*/
|
||||
export function endOfDay(date: Date = new Date()): Date {
|
||||
const result = new Date(date);
|
||||
result.setHours(23, 59, 59, 999);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add days to a date
|
||||
*/
|
||||
export function addDays(date: Date, days: number): Date {
|
||||
const result = new Date(date);
|
||||
result.setDate(result.getDate() + days);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date for German locale
|
||||
*/
|
||||
export function formatDateDE(date: Date, options?: Intl.DateTimeFormatOptions): string {
|
||||
return date.toLocaleDateString('de-DE', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time for German locale
|
||||
*/
|
||||
export function formatTimeDE(date: Date): string {
|
||||
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if date is today
|
||||
*/
|
||||
export function isToday(date: Date): boolean {
|
||||
const today = new Date();
|
||||
return (
|
||||
date.getDate() === today.getDate() &&
|
||||
date.getMonth() === today.getMonth() &&
|
||||
date.getFullYear() === today.getFullYear()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if date is tomorrow
|
||||
*/
|
||||
export function isTomorrow(date: Date): boolean {
|
||||
const tomorrow = addDays(new Date(), 1);
|
||||
return (
|
||||
date.getDate() === tomorrow.getDate() &&
|
||||
date.getMonth() === tomorrow.getMonth() &&
|
||||
date.getFullYear() === tomorrow.getFullYear()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse German date keywords
|
||||
*/
|
||||
export function parseGermanDateKeyword(keyword: string): Date | null {
|
||||
const lower = keyword.toLowerCase().trim();
|
||||
const today = startOfDay();
|
||||
|
||||
switch (lower) {
|
||||
case 'heute':
|
||||
return today;
|
||||
case 'morgen':
|
||||
return addDays(today, 1);
|
||||
case 'übermorgen':
|
||||
return addDays(today, 2);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relative date label in German
|
||||
*/
|
||||
export function getRelativeDateLabel(date: Date): string {
|
||||
if (isToday(date)) return 'Heute';
|
||||
if (isTomorrow(date)) return 'Morgen';
|
||||
return formatDateDE(date, { weekday: 'short', day: '2-digit', month: '2-digit' });
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue