feat(bot-services): add credit and gift services for Matrix bots

- Add CreditService with balance checking and consumption
- Add GiftService for gift code creation and redemption
- Add i18n support for credit/gift messages (DE/EN)
- Add Matrix bot mixins for credit and gift commands
This commit is contained in:
Till-JS 2026-02-13 23:29:21 +01:00
parent 92c6dc83ee
commit 962b942e2a
17 changed files with 2057 additions and 0 deletions

View file

@ -6,6 +6,9 @@ import {
CreditModuleOptions,
CreditStatusMessage,
CreditErrorCode,
CreditPackage,
PaymentLinkResult,
PurchaseStatusResult,
CREDIT_MODULE_OPTIONS,
} from './types';
@ -312,4 +315,159 @@ export class CreditService {
return { text, html };
}
// ============================================================================
// PACKAGE & PAYMENT LINK METHODS (for bot credit purchasing)
// ============================================================================
/**
* Get available credit packages
*
* @returns List of available credit packages
*/
async getPackages(): Promise<CreditPackage[]> {
try {
const response = await fetch(`${this.authUrl}/api/v1/credits/packages`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
this.logger.warn(`Failed to get packages: ${response.status}`);
return [];
}
const packages = (await response.json()) as Array<{
id: string;
name: string;
credits: number;
priceEuroCents: number;
sortOrder: number;
}>;
return packages.map((pkg) => ({
id: pkg.id,
name: pkg.name,
credits: pkg.credits,
priceEuroCents: pkg.priceEuroCents,
formattedPrice: this.formatPrice(pkg.priceEuroCents),
sortOrder: pkg.sortOrder,
}));
} catch (error) {
this.logger.error('Error getting packages:', error);
return [];
}
}
/**
* Create a payment link for purchasing credits
*
* @param token - User's JWT token
* @param packageId - ID of the package to purchase
* @param roomId - Optional Matrix room ID for notification after payment
* @returns Payment link result with URL and expiration
*/
async createPaymentLink(
token: string,
packageId: string,
roomId?: string
): Promise<PaymentLinkResult | null> {
try {
const response = await fetch(`${this.authUrl}/api/v1/credits/payment-link`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
packageId,
roomId,
}),
});
if (!response.ok) {
this.logger.warn(`Failed to create payment link: ${response.status}`);
return null;
}
const result = (await response.json()) as {
url: string;
purchaseId: string;
expiresAt: string;
package: {
name: string;
credits: number;
priceEuroCents: number;
};
};
return {
url: result.url,
purchaseId: result.purchaseId,
expiresAt: new Date(result.expiresAt),
package: result.package,
};
} catch (error) {
this.logger.error('Error creating payment link:', error);
return null;
}
}
/**
* Get purchase status
*
* @param token - User's JWT token
* @param purchaseId - Purchase ID to check
* @returns Purchase status or null if not found
*/
async getPurchaseStatus(token: string, purchaseId: string): Promise<PurchaseStatusResult | null> {
try {
const response = await fetch(`${this.authUrl}/api/v1/credits/purchase/${purchaseId}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
this.logger.warn(`Failed to get purchase status: ${response.status}`);
return null;
}
const result = (await response.json()) as {
id: string;
status: 'pending' | 'completed' | 'failed';
credits: number;
priceEuroCents: number;
createdAt: string;
completedAt?: string;
};
return {
id: result.id,
status: result.status,
credits: result.credits,
priceEuroCents: result.priceEuroCents,
createdAt: new Date(result.createdAt),
completedAt: result.completedAt ? new Date(result.completedAt) : undefined,
};
} catch (error) {
this.logger.error('Error getting purchase status:', error);
return null;
}
}
/**
* Format price in euro cents to human-readable format
*
* @param priceEuroCents - Price in euro cents
* @returns Formatted price (e.g., "4,99 €")
*/
private formatPrice(priceEuroCents: number): string {
const euros = priceEuroCents / 100;
return `${euros.toFixed(2).replace('.', ',')}`;
}
}

View file

@ -6,5 +6,9 @@ export type {
CreditConsumeResult,
CreditModuleOptions,
CreditStatusMessage,
CreditPackage,
PaymentLinkResult,
PurchaseStatus,
PurchaseStatusResult,
} from './types';
export { CREDIT_MODULE_OPTIONS, CreditErrorCode } from './types';

View file

@ -73,3 +73,62 @@ export interface CreditStatusMessage {
/** HTML formatted message (for Matrix) */
html: string;
}
/**
* Credit package available for purchase
*/
export interface CreditPackage {
/** Package ID */
id: string;
/** Package name (e.g., "Starter", "Standard", "Premium") */
name: string;
/** Number of credits in package */
credits: number;
/** Price in euro cents */
priceEuroCents: number;
/** Human-readable price (e.g., "4,99 €") */
formattedPrice: string;
/** Display order */
sortOrder: number;
}
/**
* Result of creating a payment link
*/
export interface PaymentLinkResult {
/** Stripe Checkout URL */
url: string;
/** Purchase ID for tracking */
purchaseId: string;
/** When the link expires */
expiresAt: Date;
/** Package details */
package: {
name: string;
credits: number;
priceEuroCents: number;
};
}
/**
* Purchase status
*/
export type PurchaseStatus = 'pending' | 'completed' | 'failed';
/**
* Purchase status result
*/
export interface PurchaseStatusResult {
/** Purchase ID */
id: string;
/** Current status */
status: PurchaseStatus;
/** Credits in package */
credits: number;
/** Price in euro cents */
priceEuroCents: number;
/** When purchase was created */
createdAt: Date;
/** When purchase was completed (if completed) */
completedAt?: Date;
}

View file

@ -0,0 +1,52 @@
import { Module, DynamicModule, Provider } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { GiftService } from './gift.service';
import { GiftModuleOptions, GIFT_MODULE_OPTIONS } from './types';
@Module({})
export class GiftModule {
/**
* Register the gift module with options
*
* @param options - Gift module options
* @returns Dynamic module
*
* @example
* ```typescript
* GiftModule.register({ authUrl: 'http://mana-core-auth:3001' })
* ```
*/
static register(options?: GiftModuleOptions): DynamicModule {
const optionsProvider: Provider = {
provide: GIFT_MODULE_OPTIONS,
useValue: options || {},
};
return {
module: GiftModule,
imports: [ConfigModule],
providers: [optionsProvider, GiftService],
exports: [GiftService],
};
}
/**
* Register the gift module with default configuration
* Uses ConfigService to get auth URL
*
* @returns Dynamic module
*
* @example
* ```typescript
* GiftModule.forRoot()
* ```
*/
static forRoot(): DynamicModule {
return {
module: GiftModule,
imports: [ConfigModule],
providers: [GiftService],
exports: [GiftService],
};
}
}

View file

@ -0,0 +1,405 @@
import { Injectable, Inject, Logger, Optional } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
CreateGiftOptions,
CreateGiftResult,
GiftCodeInfo,
RedeemGiftResult,
CreatedGiftItem,
ReceivedGiftItem,
GiftModuleOptions,
GiftStatusMessage,
GIFT_MODULE_OPTIONS,
} from './types';
/**
* Shared gift code management service for Matrix bots
*
* Provides gift code creation, redemption, and listing
* for gifting credits between users via Matrix chat.
*
* @example
* ```typescript
* // In NestJS module
* imports: [GiftModule.register({ authUrl: 'http://mana-core-auth:3001' })]
*
* // In service/controller
* const gift = await giftService.createGift(token, 50, { message: 'Happy birthday!' });
* const result = await giftService.redeemGift(token, 'ABC123');
* ```
*/
@Injectable()
export class GiftService {
private readonly logger = new Logger(GiftService.name);
private readonly authUrl: string;
constructor(
@Optional() private configService: ConfigService,
@Optional() @Inject(GIFT_MODULE_OPTIONS) private options?: GiftModuleOptions
) {
// Priority: module options > config > environment > default
this.authUrl =
options?.authUrl ||
this.configService?.get<string>('auth.url') ||
this.configService?.get<string>('MANA_CORE_AUTH_URL') ||
'http://localhost:3001';
this.logger.log(`Gift service initialized with auth URL: ${this.authUrl}`);
}
/**
* Create a new gift code
*
* @param token - User's JWT token
* @param credits - Total credits to gift
* @param options - Gift options (type, portions, message, etc.)
* @returns Created gift code info
*/
async createGift(
token: string,
credits: number,
options?: CreateGiftOptions
): Promise<CreateGiftResult | null> {
try {
const response = await fetch(`${this.authUrl}/api/v1/gifts`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
credits,
...options,
sourceAppId: 'matrix-bot',
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
this.logger.warn(`Failed to create gift: ${response.status}`, error);
return null;
}
return (await response.json()) as CreateGiftResult;
} catch (error) {
this.logger.error('Error creating gift:', error);
return null;
}
}
/**
* Get gift code info (public, for preview)
*
* @param code - The gift code
* @returns Gift code info or null if not found
*/
async getGiftInfo(code: string): Promise<GiftCodeInfo | null> {
try {
const response = await fetch(`${this.authUrl}/api/v1/gifts/${code}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
if (response.status === 404) {
return null;
}
this.logger.warn(`Failed to get gift info: ${response.status}`);
return null;
}
return (await response.json()) as GiftCodeInfo;
} catch (error) {
this.logger.error('Error getting gift info:', error);
return null;
}
}
/**
* Redeem a gift code
*
* @param token - User's JWT token
* @param code - The gift code to redeem
* @param answer - Riddle answer (if required)
* @param matrixUserId - Matrix user ID (for personalized gifts)
* @returns Redemption result
*/
async redeemGift(
token: string,
code: string,
answer?: string,
matrixUserId?: string
): Promise<RedeemGiftResult> {
try {
const headers: Record<string, string> = {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
};
if (matrixUserId) {
headers['X-Matrix-User-Id'] = matrixUserId;
}
const response = await fetch(`${this.authUrl}/api/v1/gifts/${code}/redeem`, {
method: 'POST',
headers,
body: JSON.stringify({
answer,
sourceAppId: 'matrix-bot',
}),
});
if (!response.ok && response.status !== 200) {
const errorData = (await response.json().catch(() => ({}))) as { message?: string };
return {
success: false,
error: errorData.message || 'Failed to redeem gift code',
};
}
return (await response.json()) as RedeemGiftResult;
} catch (error) {
this.logger.error('Error redeeming gift:', error);
return {
success: false,
error: 'Service temporarily unavailable',
};
}
}
/**
* Cancel a gift code and get refund
*
* @param token - User's JWT token
* @param codeId - Gift code ID to cancel
* @returns Refunded credits amount
*/
async cancelGift(token: string, codeId: string): Promise<{ refundedCredits: number } | null> {
try {
const response = await fetch(`${this.authUrl}/api/v1/gifts/${codeId}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
this.logger.warn(`Failed to cancel gift: ${response.status}`);
return null;
}
return (await response.json()) as { refundedCredits: number };
} catch (error) {
this.logger.error('Error cancelling gift:', error);
return null;
}
}
/**
* List gift codes created by the user
*
* @param token - User's JWT token
* @returns List of created gifts
*/
async listCreatedGifts(token: string): Promise<CreatedGiftItem[]> {
try {
const response = await fetch(`${this.authUrl}/api/v1/gifts/me/created`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
this.logger.warn(`Failed to list created gifts: ${response.status}`);
return [];
}
return (await response.json()) as CreatedGiftItem[];
} catch (error) {
this.logger.error('Error listing created gifts:', error);
return [];
}
}
/**
* List gifts received by the user
*
* @param token - User's JWT token
* @returns List of received gifts
*/
async listReceivedGifts(token: string): Promise<ReceivedGiftItem[]> {
try {
const response = await fetch(`${this.authUrl}/api/v1/gifts/me/received`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
this.logger.warn(`Failed to list received gifts: ${response.status}`);
return [];
}
return (await response.json()) as ReceivedGiftItem[];
} catch (error) {
this.logger.error('Error listing received gifts:', error);
return [];
}
}
// ============================================================================
// MESSAGE FORMATTING HELPERS
// ============================================================================
/**
* Format a gift created success message
*/
formatGiftCreatedMessage(gift: CreateGiftResult): GiftStatusMessage {
const lines: string[] = [];
const htmlLines: string[] = [];
lines.push('🎁 **Geschenk erstellt!**');
htmlLines.push('🎁 <b>Geschenk erstellt!</b>');
lines.push('');
htmlLines.push('<br>');
lines.push(`Code: \`${gift.code}\``);
htmlLines.push(`Code: <code>${gift.code}</code>`);
lines.push(`Credits: ${gift.creditsPerPortion}${gift.totalPortions > 1 ? ` × ${gift.totalPortions}` : ''}`);
htmlLines.push(`Credits: ${gift.creditsPerPortion}${gift.totalPortions > 1 ? ` × ${gift.totalPortions}` : ''}`);
lines.push('');
htmlLines.push('<br>');
lines.push(`Link: ${gift.url}`);
htmlLines.push(`Link: <a href="${gift.url}">${gift.url}</a>`);
return {
text: lines.join('\n'),
html: htmlLines.join('<br>'),
};
}
/**
* Format a gift redeemed success message
*/
formatGiftRedeemedMessage(
credits: number,
newBalance: number,
message?: string
): GiftStatusMessage {
const lines: string[] = [];
const htmlLines: string[] = [];
lines.push('🎁 **Geschenk eingelöst!**');
htmlLines.push('🎁 <b>Geschenk eingelöst!</b>');
lines.push(`+${credits} Credits`);
htmlLines.push(`+${credits} Credits`);
if (message) {
lines.push('');
lines.push(`"${message}"`);
htmlLines.push('<br>');
htmlLines.push(`<i>"${message}"</i>`);
}
lines.push('');
lines.push(`Neuer Kontostand: ${newBalance.toFixed(2)} Credits`);
htmlLines.push('<br>');
htmlLines.push(`Neuer Kontostand: ${newBalance.toFixed(2)} Credits`);
return {
text: lines.join('\n'),
html: htmlLines.join('<br>'),
};
}
/**
* Format gift list message
*/
formatGiftListMessage(gifts: CreatedGiftItem[]): GiftStatusMessage {
const lines: string[] = [];
const htmlLines: string[] = [];
lines.push('🎁 **Deine Geschenke:**');
htmlLines.push('🎁 <b>Deine Geschenke:</b>');
lines.push('');
htmlLines.push('<br>');
if (gifts.length === 0) {
lines.push('Keine aktiven Geschenke.');
htmlLines.push('Keine aktiven Geschenke.');
} else {
gifts.forEach((gift, index) => {
const statusIcon = gift.status === 'active' ? '✅' : gift.status === 'depleted' ? '✓' : '❌';
const claimed = `${gift.claimedPortions}/${gift.totalPortions}`;
lines.push(`${index + 1}. \`${gift.code}\` ${statusIcon} ${gift.creditsPerPortion} Cr · ${claimed}`);
htmlLines.push(`${index + 1}. <code>${gift.code}</code> ${statusIcon} ${gift.creditsPerPortion} Cr · ${claimed}`);
});
}
return {
text: lines.join('\n'),
html: htmlLines.join('<br>'),
};
}
/**
* Format gift code info message (for preview before redeeming)
*/
formatGiftInfoMessage(info: GiftCodeInfo): GiftStatusMessage {
const lines: string[] = [];
const htmlLines: string[] = [];
lines.push('🎁 **Geschenk-Info:**');
htmlLines.push('🎁 <b>Geschenk-Info:</b>');
lines.push(`Credits: ${info.creditsPerPortion}`);
htmlLines.push(`Credits: ${info.creditsPerPortion}`);
if (info.totalPortions > 1) {
lines.push(`Verfügbar: ${info.remainingPortions}/${info.totalPortions}`);
htmlLines.push(`Verfügbar: ${info.remainingPortions}/${info.totalPortions}`);
}
if (info.message) {
lines.push('');
lines.push(`"${info.message}"`);
htmlLines.push('<br>');
htmlLines.push(`<i>"${info.message}"</i>`);
}
if (info.hasRiddle) {
lines.push('');
lines.push(`${info.riddleQuestion}`);
lines.push('Antworte mit: `!einloesen CODE antwort`');
htmlLines.push('<br>');
htmlLines.push(`${info.riddleQuestion}`);
htmlLines.push('Antworte mit: <code>!einloesen CODE antwort</code>');
}
if (info.creatorName) {
lines.push('');
lines.push(`Von: ${info.creatorName}`);
htmlLines.push('<br>');
htmlLines.push(`Von: ${info.creatorName}`);
}
return {
text: lines.join('\n'),
html: htmlLines.join('<br>'),
};
}
}

View file

@ -0,0 +1,15 @@
export { GiftService } from './gift.service';
export { GiftModule } from './gift.module';
export type {
GiftCodeType,
GiftCodeStatus,
CreateGiftOptions,
CreateGiftResult,
GiftCodeInfo,
RedeemGiftResult,
CreatedGiftItem,
ReceivedGiftItem,
GiftModuleOptions,
GiftStatusMessage,
} from './types';
export { GIFT_MODULE_OPTIONS } from './types';

View file

@ -0,0 +1,173 @@
/**
* Types for gift code management in Matrix bots
*/
/**
* Gift code types
*/
export type GiftCodeType = 'simple' | 'personalized' | 'split' | 'first_come' | 'riddle';
/**
* Gift code status
*/
export type GiftCodeStatus = 'active' | 'depleted' | 'expired' | 'cancelled' | 'refunded';
/**
* Options for creating a gift code
*/
export interface CreateGiftOptions {
/** Gift type (default: 'simple') */
type?: GiftCodeType;
/** Number of portions for split/first_come (default: 1) */
portions?: number;
/** Target email for personalized gifts */
targetEmail?: string;
/** Target Matrix ID for personalized gifts */
targetMatrixId?: string;
/** Riddle question */
riddleQuestion?: string;
/** Riddle answer */
riddleAnswer?: string;
/** Optional message */
message?: string;
/** Expiration date (ISO string) */
expiresAt?: string;
}
/**
* Result of creating a gift code
*/
export interface CreateGiftResult {
/** Gift code ID */
id: string;
/** The gift code (6 chars) */
code: string;
/** Short URL (mana.how/g/CODE) */
url: string;
/** Total credits reserved */
totalCredits: number;
/** Credits per portion */
creditsPerPortion: number;
/** Total number of portions */
totalPortions: number;
/** Gift type */
type: GiftCodeType;
/** Expiration date */
expiresAt?: string;
}
/**
* Gift code info (public, for preview)
*/
export interface GiftCodeInfo {
/** The gift code */
code: string;
/** Gift type */
type: GiftCodeType;
/** Current status */
status: GiftCodeStatus;
/** Credits per portion */
creditsPerPortion: number;
/** Total portions */
totalPortions: number;
/** Claimed portions */
claimedPortions: number;
/** Remaining portions */
remainingPortions: number;
/** Optional message */
message?: string;
/** Riddle question (if riddle type) */
riddleQuestion?: string;
/** Whether gift has a riddle */
hasRiddle: boolean;
/** Whether gift is for a specific person */
isPersonalized: boolean;
/** Expiration date */
expiresAt?: string;
/** Creator name */
creatorName?: string;
}
/**
* Result of redeeming a gift code
*/
export interface RedeemGiftResult {
/** Whether redemption was successful */
success: boolean;
/** Credits received (if successful) */
credits?: number;
/** Gift message (if any) */
message?: string;
/** Error message (if failed) */
error?: string;
/** New credit balance */
newBalance?: number;
}
/**
* Gift code in user's created list
*/
export interface CreatedGiftItem {
/** Gift code ID */
id: string;
/** The gift code */
code: string;
/** Short URL */
url: string;
/** Gift type */
type: GiftCodeType;
/** Current status */
status: GiftCodeStatus;
/** Total credits reserved */
totalCredits: number;
/** Credits per portion */
creditsPerPortion: number;
/** Total portions */
totalPortions: number;
/** Claimed portions */
claimedPortions: number;
/** Optional message */
message?: string;
/** Expiration date */
expiresAt?: string;
/** Creation date */
createdAt: string;
}
/**
* Gift in user's received list
*/
export interface ReceivedGiftItem {
/** Redemption ID */
id: string;
/** The gift code */
code: string;
/** Credits received */
credits: number;
/** Gift message */
message?: string;
/** Creator name */
creatorName?: string;
/** When redeemed */
redeemedAt: string;
}
/**
* Gift module configuration options
*/
export interface GiftModuleOptions {
/** Mana Core Auth URL */
authUrl?: string;
}
export const GIFT_MODULE_OPTIONS = 'GIFT_MODULE_OPTIONS';
/**
* Formatted gift message for Matrix bots
*/
export interface GiftStatusMessage {
/** Plain text message */
text: string;
/** HTML formatted message (for Matrix) */
html: string;
}

View file

@ -7,6 +7,7 @@ import {
CalendarTranslations,
ContactsTranslations,
ClockTranslations,
GiftTranslations,
I18nOptions,
} from './types';
import { de } from './locales/de';
@ -178,6 +179,17 @@ export class I18nService {
return (key, params) => this.interpolate(t[key], params);
}
/**
* Get a translator function for gift commands
*/
async getGiftTranslator(
userId: string
): Promise<(key: keyof GiftTranslations, params?: Record<string, string | number>) => string> {
const lang = await this.getLanguage(userId);
const t = translations[lang].gift;
return (key, params) => this.interpolate(t[key], params);
}
/**
* Get translations directly for a bot type
*/
@ -201,6 +213,11 @@ export class I18nService {
return translations[lang].clock;
}
async getGiftTranslations(userId: string): Promise<GiftTranslations> {
const lang = await this.getLanguage(userId);
return translations[lang].gift;
}
/**
* Interpolate placeholders in a string
*

View file

@ -19,6 +19,19 @@ export const de: BotTranslations = {
insufficientCredits: 'Nicht genügend Credits. Benötigt: {required}, Verfügbar: {available}',
buyCredits: 'Credits kaufen: https://mana.how/credits',
// Credit purchasing
creditBalance: 'Dein Guthaben: **{balance}** Credits',
creditPackagesTitle: '**Credit-Pakete:**',
creditPackageLine: '{num}. {name} · {credits} Credits · {price}',
creditBuyHelp: 'Kaufen: `!kaufen {num}` oder `!buy {num}`',
creditPaymentLink: 'Klicke hier um zu bezahlen:',
creditLinkValid: 'Link gültig für 24 Stunden.',
creditPaymentSuccess: '✓ Zahlung erfolgreich! **{credits}** Credits wurden deinem Konto gutgeschrieben.',
creditNewBalance: 'Neuer Kontostand: **{balance}** Credits',
creditPackageNotFound: 'Paket nicht gefunden. Nutze `!pakete` um verfügbare Pakete anzuzeigen.',
creditPurchaseError: 'Fehler beim Erstellen des Zahlungslinks. Bitte versuche es später erneut.',
creditNoPackages: 'Keine Credit-Pakete verfügbar.',
// Sync
synced: 'Synchronisiert',
localStorage: 'Lokaler Speicher',
@ -62,6 +75,17 @@ export const de: BotTranslations = {
creditsRemaining: '{amount} verbleibend',
insufficientCredits: 'Nicht genügend Credits. Benötigt: {required}, Verfügbar: {available}',
buyCredits: 'Credits kaufen: https://mana.how/credits',
creditBalance: 'Dein Guthaben: **{balance}** Credits',
creditPackagesTitle: '**Credit-Pakete:**',
creditPackageLine: '{num}. {name} · {credits} Credits · {price}',
creditBuyHelp: 'Kaufen: `!kaufen {num}` oder `!buy {num}`',
creditPaymentLink: 'Klicke hier um zu bezahlen:',
creditLinkValid: 'Link gültig für 24 Stunden.',
creditPaymentSuccess: '✓ Zahlung erfolgreich! **{credits}** Credits wurden deinem Konto gutgeschrieben.',
creditNewBalance: 'Neuer Kontostand: **{balance}** Credits',
creditPackageNotFound: 'Paket nicht gefunden. Nutze `!pakete` um verfügbare Pakete anzuzeigen.',
creditPurchaseError: 'Fehler beim Erstellen des Zahlungslinks. Bitte versuche es später erneut.',
creditNoPackages: 'Keine Credit-Pakete verfügbar.',
synced: 'Synchronisiert',
localStorage: 'Lokaler Speicher',
status: 'Status',
@ -146,6 +170,17 @@ export const de: BotTranslations = {
creditsRemaining: '{amount} verbleibend',
insufficientCredits: 'Nicht genügend Credits. Benötigt: {required}, Verfügbar: {available}',
buyCredits: 'Credits kaufen: https://mana.how/credits',
creditBalance: 'Dein Guthaben: **{balance}** Credits',
creditPackagesTitle: '**Credit-Pakete:**',
creditPackageLine: '{num}. {name} · {credits} Credits · {price}',
creditBuyHelp: 'Kaufen: `!kaufen {num}` oder `!buy {num}`',
creditPaymentLink: 'Klicke hier um zu bezahlen:',
creditLinkValid: 'Link gültig für 24 Stunden.',
creditPaymentSuccess: '✓ Zahlung erfolgreich! **{credits}** Credits wurden deinem Konto gutgeschrieben.',
creditNewBalance: 'Neuer Kontostand: **{balance}** Credits',
creditPackageNotFound: 'Paket nicht gefunden. Nutze `!pakete` um verfügbare Pakete anzuzeigen.',
creditPurchaseError: 'Fehler beim Erstellen des Zahlungslinks. Bitte versuche es später erneut.',
creditNoPackages: 'Keine Credit-Pakete verfügbar.',
synced: 'Synchronisiert',
localStorage: 'Lokaler Speicher',
status: 'Status',
@ -230,6 +265,17 @@ export const de: BotTranslations = {
creditsRemaining: '{amount} verbleibend',
insufficientCredits: 'Nicht genügend Credits. Benötigt: {required}, Verfügbar: {available}',
buyCredits: 'Credits kaufen: https://mana.how/credits',
creditBalance: 'Dein Guthaben: **{balance}** Credits',
creditPackagesTitle: '**Credit-Pakete:**',
creditPackageLine: '{num}. {name} · {credits} Credits · {price}',
creditBuyHelp: 'Kaufen: `!kaufen {num}` oder `!buy {num}`',
creditPaymentLink: 'Klicke hier um zu bezahlen:',
creditLinkValid: 'Link gültig für 24 Stunden.',
creditPaymentSuccess: '✓ Zahlung erfolgreich! **{credits}** Credits wurden deinem Konto gutgeschrieben.',
creditNewBalance: 'Neuer Kontostand: **{balance}** Credits',
creditPackageNotFound: 'Paket nicht gefunden. Nutze `!pakete` um verfügbare Pakete anzuzeigen.',
creditPurchaseError: 'Fehler beim Erstellen des Zahlungslinks. Bitte versuche es später erneut.',
creditNoPackages: 'Keine Credit-Pakete verfügbar.',
synced: 'Synchronisiert',
localStorage: 'Lokaler Speicher',
status: 'Status',
@ -314,6 +360,17 @@ export const de: BotTranslations = {
creditsRemaining: '{amount} verbleibend',
insufficientCredits: 'Nicht genügend Credits. Benötigt: {required}, Verfügbar: {available}',
buyCredits: 'Credits kaufen: https://mana.how/credits',
creditBalance: 'Dein Guthaben: **{balance}** Credits',
creditPackagesTitle: '**Credit-Pakete:**',
creditPackageLine: '{num}. {name} · {credits} Credits · {price}',
creditBuyHelp: 'Kaufen: `!kaufen {num}` oder `!buy {num}`',
creditPaymentLink: 'Klicke hier um zu bezahlen:',
creditLinkValid: 'Link gültig für 24 Stunden.',
creditPaymentSuccess: '✓ Zahlung erfolgreich! **{credits}** Credits wurden deinem Konto gutgeschrieben.',
creditNewBalance: 'Neuer Kontostand: **{balance}** Credits',
creditPackageNotFound: 'Paket nicht gefunden. Nutze `!pakete` um verfügbare Pakete anzuzeigen.',
creditPurchaseError: 'Fehler beim Erstellen des Zahlungslinks. Bitte versuche es später erneut.',
creditNoPackages: 'Keine Credit-Pakete verfügbar.',
synced: 'Synchronisiert',
localStorage: 'Lokaler Speicher',
status: 'Status',
@ -387,4 +444,107 @@ export const de: BotTranslations = {
couldNotParseDuration: 'Konnte Zeit nicht verstehen.',
couldNotParseTime: 'Konnte Uhrzeit nicht verstehen.',
},
gift: {
// Inherit common
error: 'Fehler',
errorOccurred: 'Ein Fehler ist aufgetreten. Bitte versuche es erneut.',
notLoggedIn: 'Du bist nicht angemeldet.',
loginRequired: 'Bitte melde dich zuerst an mit `!login email passwort`',
loginSuccess: 'Erfolgreich angemeldet als **{email}**',
loginFailed: 'Anmeldung fehlgeschlagen: {error}',
logoutSuccess: 'Erfolgreich abgemeldet.',
invalidCommand: 'Unbekannter Befehl: {command}',
helpHint: 'Sag "hilfe" für alle Befehle.',
credits: 'Credits',
creditsRemaining: '{amount} verbleibend',
insufficientCredits: 'Nicht genügend Credits. Benötigt: {required}, Verfügbar: {available}',
buyCredits: 'Credits kaufen: https://mana.how/credits',
creditBalance: 'Dein Guthaben: **{balance}** Credits',
creditPackagesTitle: '**Credit-Pakete:**',
creditPackageLine: '{num}. {name} · {credits} Credits · {price}',
creditBuyHelp: 'Kaufen: `!kaufen {num}` oder `!buy {num}`',
creditPaymentLink: 'Klicke hier um zu bezahlen:',
creditLinkValid: 'Link gültig für 24 Stunden.',
creditPaymentSuccess: '✓ Zahlung erfolgreich! **{credits}** Credits wurden deinem Konto gutgeschrieben.',
creditNewBalance: 'Neuer Kontostand: **{balance}** Credits',
creditPackageNotFound: 'Paket nicht gefunden. Nutze `!pakete` um verfügbare Pakete anzuzeigen.',
creditPurchaseError: 'Fehler beim Erstellen des Zahlungslinks. Bitte versuche es später erneut.',
creditNoPackages: 'Keine Credit-Pakete verfügbar.',
synced: 'Synchronisiert',
localStorage: 'Lokaler Speicher',
status: 'Status',
online: 'Online',
offline: 'Offline',
loggedInAs: 'Angemeldet als: {email}',
notLoggedInStatus: 'Nicht angemeldet',
languageChanged: 'Sprache geändert zu: **{language}**',
currentLanguage: 'Aktuelle Sprache: **{language}**',
availableLanguages: 'Verfügbare Sprachen: {languages}',
today: 'Heute',
tomorrow: 'Morgen',
dayAfterTomorrow: 'Übermorgen',
created: 'Erstellt',
deleted: 'Gelöscht',
updated: 'Aktualisiert',
completed: 'Erledigt',
// Gift creation
giftCreated: '🎁 **Geschenk erstellt!**',
giftCreatedCode: 'Code: `{code}`',
giftCreatedCredits: 'Credits: {credits}',
giftCreatedLink: 'Link: {url}',
giftCreatedSplit: 'Credits: {credits} × {portions}',
giftInvalidCredits: 'Bitte gib eine gültige Credit-Anzahl an (1-10000).',
giftInvalidSyntax: 'Ungültige Syntax. Beispiel: `!geschenk 50` oder `!geschenk 100 /5`',
giftInsufficientCredits: 'Nicht genug Credits. Verfügbar: {available}',
// Gift redemption
giftRedeemed: '🎁 **Geschenk eingelöst!**',
giftRedeemedCredits: '+{credits} Credits',
giftRedeemedMessage: '"{message}"',
giftInvalidCode: 'Geschenkcode nicht gefunden.',
giftExpired: 'Dieser Geschenkcode ist abgelaufen.',
giftDepleted: 'Dieser Geschenkcode wurde bereits vollständig eingelöst.',
giftAlreadyClaimed: 'Du hast dieses Geschenk bereits eingelöst.',
giftWrongUser: 'Dieser Geschenkcode ist für eine bestimmte Person.',
giftWrongAnswer: 'Falsche Antwort. Versuche es erneut.',
giftRiddleRequired: 'Bitte gib die Antwort auf das Rätsel an.',
giftRiddleQuestion: '❓ {question}',
// Gift list
giftListTitle: '🎁 **Deine Geschenke:**',
giftListEmpty: 'Keine aktiven Geschenke.',
giftListItem: '{num}. `{code}` {status} {credits} Cr · {claimed}/{total}',
giftReceivedListTitle: '🎁 **Erhaltene Geschenke:**',
giftReceivedListEmpty: 'Keine erhaltenen Geschenke.',
// Gift info
giftInfoTitle: '🎁 **Geschenk-Info:**',
giftInfoCredits: 'Credits: {credits}',
giftInfoAvailable: 'Verfügbar: {remaining}/{total}',
giftInfoFrom: 'Von: {name}',
// Gift help
giftHelpTitle: 'Geschenke - Hilfe',
giftHelpCommands: `**Befehle:**
\`!geschenk [credits]\` - Geschenkcode erstellen
\`!geschenk [credits] /[anzahl]\` - Split-Geschenk
\`!geschenk [credits] @email\` - Personalisiert
\`!geschenk [credits] ?="antwort"\` - Mit Rätsel
\`!einloesen [code]\` - Code einlösen
\`!meine-geschenke\` - Deine Geschenke anzeigen`,
giftHelpSyntax: `**Syntax:**
\`!geschenk 50\` - Einfaches Geschenk
\`!geschenk 100 /5\` - 5 Portionen à 20 Cr
\`!geschenk 50 "Alles Gute!"\` - Mit Nachricht`,
giftHelpExamples: `**Beispiele:**
\`!geschenk 50\`
\`!geschenk 100 /5 Teilt euch!\`
\`!einloesen ABC123\``,
// Gift cancellation
giftCancelled: 'Geschenk storniert.',
giftRefunded: '{credits} Credits zurückerstattet.',
},
};

View file

@ -19,6 +19,19 @@ export const en: BotTranslations = {
insufficientCredits: 'Insufficient credits. Required: {required}, Available: {available}',
buyCredits: 'Buy credits: https://mana.how/credits',
// Credit purchasing
creditBalance: 'Your balance: **{balance}** credits',
creditPackagesTitle: '**Credit packages:**',
creditPackageLine: '{num}. {name} · {credits} credits · {price}',
creditBuyHelp: 'Purchase: `!buy {num}` or `!kaufen {num}`',
creditPaymentLink: 'Click here to pay:',
creditLinkValid: 'Link valid for 24 hours.',
creditPaymentSuccess: '✓ Payment successful! **{credits}** credits have been added to your account.',
creditNewBalance: 'New balance: **{balance}** credits',
creditPackageNotFound: 'Package not found. Use `!packages` to view available packages.',
creditPurchaseError: 'Error creating payment link. Please try again later.',
creditNoPackages: 'No credit packages available.',
// Sync
synced: 'Synced',
localStorage: 'Local storage',
@ -62,6 +75,17 @@ export const en: BotTranslations = {
creditsRemaining: '{amount} remaining',
insufficientCredits: 'Insufficient credits. Required: {required}, Available: {available}',
buyCredits: 'Buy credits: https://mana.how/credits',
creditBalance: 'Your balance: **{balance}** credits',
creditPackagesTitle: '**Credit packages:**',
creditPackageLine: '{num}. {name} · {credits} credits · {price}',
creditBuyHelp: 'Purchase: `!buy {num}` or `!kaufen {num}`',
creditPaymentLink: 'Click here to pay:',
creditLinkValid: 'Link valid for 24 hours.',
creditPaymentSuccess: '✓ Payment successful! **{credits}** credits have been added to your account.',
creditNewBalance: 'New balance: **{balance}** credits',
creditPackageNotFound: 'Package not found. Use `!packages` to view available packages.',
creditPurchaseError: 'Error creating payment link. Please try again later.',
creditNoPackages: 'No credit packages available.',
synced: 'Synced',
localStorage: 'Local storage',
status: 'Status',
@ -146,6 +170,17 @@ export const en: BotTranslations = {
creditsRemaining: '{amount} remaining',
insufficientCredits: 'Insufficient credits. Required: {required}, Available: {available}',
buyCredits: 'Buy credits: https://mana.how/credits',
creditBalance: 'Your balance: **{balance}** credits',
creditPackagesTitle: '**Credit packages:**',
creditPackageLine: '{num}. {name} · {credits} credits · {price}',
creditBuyHelp: 'Purchase: `!buy {num}` or `!kaufen {num}`',
creditPaymentLink: 'Click here to pay:',
creditLinkValid: 'Link valid for 24 hours.',
creditPaymentSuccess: '✓ Payment successful! **{credits}** credits have been added to your account.',
creditNewBalance: 'New balance: **{balance}** credits',
creditPackageNotFound: 'Package not found. Use `!packages` to view available packages.',
creditPurchaseError: 'Error creating payment link. Please try again later.',
creditNoPackages: 'No credit packages available.',
synced: 'Synced',
localStorage: 'Local storage',
status: 'Status',
@ -230,6 +265,17 @@ export const en: BotTranslations = {
creditsRemaining: '{amount} remaining',
insufficientCredits: 'Insufficient credits. Required: {required}, Available: {available}',
buyCredits: 'Buy credits: https://mana.how/credits',
creditBalance: 'Your balance: **{balance}** credits',
creditPackagesTitle: '**Credit packages:**',
creditPackageLine: '{num}. {name} · {credits} credits · {price}',
creditBuyHelp: 'Purchase: `!buy {num}` or `!kaufen {num}`',
creditPaymentLink: 'Click here to pay:',
creditLinkValid: 'Link valid for 24 hours.',
creditPaymentSuccess: '✓ Payment successful! **{credits}** credits have been added to your account.',
creditNewBalance: 'New balance: **{balance}** credits',
creditPackageNotFound: 'Package not found. Use `!packages` to view available packages.',
creditPurchaseError: 'Error creating payment link. Please try again later.',
creditNoPackages: 'No credit packages available.',
synced: 'Synced',
localStorage: 'Local storage',
status: 'Status',
@ -314,6 +360,17 @@ export const en: BotTranslations = {
creditsRemaining: '{amount} remaining',
insufficientCredits: 'Insufficient credits. Required: {required}, Available: {available}',
buyCredits: 'Buy credits: https://mana.how/credits',
creditBalance: 'Your balance: **{balance}** credits',
creditPackagesTitle: '**Credit packages:**',
creditPackageLine: '{num}. {name} · {credits} credits · {price}',
creditBuyHelp: 'Purchase: `!buy {num}` or `!kaufen {num}`',
creditPaymentLink: 'Click here to pay:',
creditLinkValid: 'Link valid for 24 hours.',
creditPaymentSuccess: '✓ Payment successful! **{credits}** credits have been added to your account.',
creditNewBalance: 'New balance: **{balance}** credits',
creditPackageNotFound: 'Package not found. Use `!packages` to view available packages.',
creditPurchaseError: 'Error creating payment link. Please try again later.',
creditNoPackages: 'No credit packages available.',
synced: 'Synced',
localStorage: 'Local storage',
status: 'Status',
@ -387,4 +444,107 @@ export const en: BotTranslations = {
couldNotParseDuration: 'Could not parse duration.',
couldNotParseTime: 'Could not parse time.',
},
gift: {
// Inherit common
error: 'Error',
errorOccurred: 'An error occurred. Please try again.',
notLoggedIn: 'You are not logged in.',
loginRequired: 'Please log in first with `!login email password`',
loginSuccess: 'Successfully logged in as **{email}**',
loginFailed: 'Login failed: {error}',
logoutSuccess: 'Successfully logged out.',
invalidCommand: 'Unknown command: {command}',
helpHint: 'Say "help" for all commands.',
credits: 'Credits',
creditsRemaining: '{amount} remaining',
insufficientCredits: 'Insufficient credits. Required: {required}, Available: {available}',
buyCredits: 'Buy credits: https://mana.how/credits',
creditBalance: 'Your balance: **{balance}** credits',
creditPackagesTitle: '**Credit packages:**',
creditPackageLine: '{num}. {name} · {credits} credits · {price}',
creditBuyHelp: 'Purchase: `!buy {num}` or `!kaufen {num}`',
creditPaymentLink: 'Click here to pay:',
creditLinkValid: 'Link valid for 24 hours.',
creditPaymentSuccess: '✓ Payment successful! **{credits}** credits have been added to your account.',
creditNewBalance: 'New balance: **{balance}** credits',
creditPackageNotFound: 'Package not found. Use `!packages` to view available packages.',
creditPurchaseError: 'Error creating payment link. Please try again later.',
creditNoPackages: 'No credit packages available.',
synced: 'Synced',
localStorage: 'Local storage',
status: 'Status',
online: 'Online',
offline: 'Offline',
loggedInAs: 'Logged in as: {email}',
notLoggedInStatus: 'Not logged in',
languageChanged: 'Language changed to: **{language}**',
currentLanguage: 'Current language: **{language}**',
availableLanguages: 'Available languages: {languages}',
today: 'Today',
tomorrow: 'Tomorrow',
dayAfterTomorrow: 'Day after tomorrow',
created: 'Created',
deleted: 'Deleted',
updated: 'Updated',
completed: 'Completed',
// Gift creation
giftCreated: '🎁 **Gift created!**',
giftCreatedCode: 'Code: `{code}`',
giftCreatedCredits: 'Credits: {credits}',
giftCreatedLink: 'Link: {url}',
giftCreatedSplit: 'Credits: {credits} × {portions}',
giftInvalidCredits: 'Please enter a valid credit amount (1-10000).',
giftInvalidSyntax: 'Invalid syntax. Example: `!gift 50` or `!gift 100 /5`',
giftInsufficientCredits: 'Insufficient credits. Available: {available}',
// Gift redemption
giftRedeemed: '🎁 **Gift redeemed!**',
giftRedeemedCredits: '+{credits} credits',
giftRedeemedMessage: '"{message}"',
giftInvalidCode: 'Gift code not found.',
giftExpired: 'This gift code has expired.',
giftDepleted: 'This gift code has been fully claimed.',
giftAlreadyClaimed: 'You have already claimed this gift.',
giftWrongUser: 'This gift code is for a specific person.',
giftWrongAnswer: 'Wrong answer. Try again.',
giftRiddleRequired: 'Please provide the answer to the riddle.',
giftRiddleQuestion: '❓ {question}',
// Gift list
giftListTitle: '🎁 **Your gifts:**',
giftListEmpty: 'No active gifts.',
giftListItem: '{num}. `{code}` {status} {credits} Cr · {claimed}/{total}',
giftReceivedListTitle: '🎁 **Received gifts:**',
giftReceivedListEmpty: 'No received gifts.',
// Gift info
giftInfoTitle: '🎁 **Gift info:**',
giftInfoCredits: 'Credits: {credits}',
giftInfoAvailable: 'Available: {remaining}/{total}',
giftInfoFrom: 'From: {name}',
// Gift help
giftHelpTitle: 'Gifts - Help',
giftHelpCommands: `**Commands:**
\`!gift [credits]\` - Create gift code
\`!gift [credits] /[count]\` - Split gift
\`!gift [credits] @email\` - Personalized
\`!gift [credits] ?="answer"\` - With riddle
\`!redeem [code]\` - Redeem code
\`!my-gifts\` - Show your gifts`,
giftHelpSyntax: `**Syntax:**
\`!gift 50\` - Simple gift
\`!gift 100 /5\` - 5 portions of 20 Cr
\`!gift 50 "Happy birthday!"\` - With message`,
giftHelpExamples: `**Examples:**
\`!gift 50\`
\`!gift 100 /5 Share this!\`
\`!redeem ABC123\``,
// Gift cancellation
giftCancelled: 'Gift cancelled.',
giftRefunded: '{credits} credits refunded.',
},
};

View file

@ -24,6 +24,19 @@ export interface CommonTranslations {
insufficientCredits: string;
buyCredits: string;
// Credit purchasing
creditBalance: string;
creditPackagesTitle: string;
creditPackageLine: string;
creditBuyHelp: string;
creditPaymentLink: string;
creditLinkValid: string;
creditPaymentSuccess: string;
creditNewBalance: string;
creditPackageNotFound: string;
creditPurchaseError: string;
creditNoPackages: string;
// Sync
synced: string;
localStorage: string;
@ -216,6 +229,57 @@ export interface ClockTranslations extends CommonTranslations {
couldNotParseTime: string;
}
/**
* Gift translations
*/
export interface GiftTranslations extends CommonTranslations {
// Gift creation
giftCreated: string;
giftCreatedCode: string;
giftCreatedCredits: string;
giftCreatedLink: string;
giftCreatedSplit: string;
giftInvalidCredits: string;
giftInvalidSyntax: string;
giftInsufficientCredits: string;
// Gift redemption
giftRedeemed: string;
giftRedeemedCredits: string;
giftRedeemedMessage: string;
giftInvalidCode: string;
giftExpired: string;
giftDepleted: string;
giftAlreadyClaimed: string;
giftWrongUser: string;
giftWrongAnswer: string;
giftRiddleRequired: string;
giftRiddleQuestion: string;
// Gift list
giftListTitle: string;
giftListEmpty: string;
giftListItem: string;
giftReceivedListTitle: string;
giftReceivedListEmpty: string;
// Gift info
giftInfoTitle: string;
giftInfoCredits: string;
giftInfoAvailable: string;
giftInfoFrom: string;
// Gift help
giftHelpTitle: string;
giftHelpCommands: string;
giftHelpSyntax: string;
giftHelpExamples: string;
// Gift cancellation
giftCancelled: string;
giftRefunded: string;
}
/**
* All bot translations combined
*/
@ -225,6 +289,7 @@ export interface BotTranslations {
calendar: CalendarTranslations;
contacts: ContactsTranslations;
clock: ClockTranslations;
gift: GiftTranslations;
}
/**

View file

@ -134,8 +134,27 @@ export type {
CreditConsumeResult,
CreditModuleOptions,
CreditStatusMessage,
CreditPackage,
PaymentLinkResult,
PurchaseStatus,
PurchaseStatusResult,
} from './credit/index.js';
// Gift (Gift code management for Matrix bots)
export { GiftModule, GiftService, GIFT_MODULE_OPTIONS } from './gift/index.js';
export type {
GiftCodeType,
GiftCodeStatus,
CreateGiftOptions,
CreateGiftResult,
GiftCodeInfo,
RedeemGiftResult,
CreatedGiftItem,
ReceivedGiftItem,
GiftModuleOptions,
GiftStatusMessage,
} from './gift/index.js';
// I18n (Multi-language support for Matrix bots)
export { I18nModule, I18nService, I18N_OPTIONS, LANGUAGE_NAMES } from './i18n/index.js';
export type {
@ -147,6 +166,7 @@ export type {
CalendarTranslations,
ContactsTranslations,
ClockTranslations,
GiftTranslations,
} from './i18n/index.js';
export { de as deTranslations, en as enTranslations } from './i18n/index.js';