Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ PUBLIC_KEY=your_discord_public_key

# Google AI Configuration
GEMINI_API_KEY=your_google_genai_api_key

MYSQL_ROOT_PASSWORD=
MYSQL_DATABASE=
MYSQL_USER=
MYSQL_PASSWORD=
25 changes: 25 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
version: '3.8'

services:
mysql:
image: mysql:8.0
container_name: message-translator-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword}
MYSQL_DATABASE: ${MYSQL_DATABASE:-translator_db}
MYSQL_USER: ${MYSQL_USER:-translator}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-translator_password}
volumes:
- mysql-data:/var/lib/mysql
networks:
- app-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-rootpassword}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s

message-translator:
build:
context: .
Expand All @@ -12,6 +32,9 @@ services:
- "3000:3000"
env_file:
- .env
depends_on:
mysql:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
Expand All @@ -21,6 +44,10 @@ services:
networks:
- app-network

volumes:
mysql-data:
driver: local

networks:
app-network:
driver: bridge
23 changes: 23 additions & 0 deletions mysql-shell.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash

# Script to access MySQL container shell

CONTAINER_NAME="message-translator-mysql"
MYSQL_USER="${MYSQL_USER:-translator}"
MYSQL_PASSWORD="${MYSQL_PASSWORD:-translator_password}"
MYSQL_DATABASE="${MYSQL_DATABASE:-translator_db}"

echo "Connecting to MySQL container: $CONTAINER_NAME"
echo "Database: $MYSQL_DATABASE"
echo "User: $MYSQL_USER"
echo ""

# Check if container is running
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
echo "Error: Container '$CONTAINER_NAME' is not running."
echo "Please start the container first with: docker-compose up -d"
exit 1
fi

# Connect to MySQL
docker exec -it $CONTAINER_NAME mysql -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@google/genai": "^1.33.0",
"discord-interactions": "^4.4.0",
"discord.js": "^14.25.1",
"mysql2": "^3.15.3",
"tweetnacl": "^1.0.3"
}
}
34 changes: 33 additions & 1 deletion src/gemini.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,38 @@
import { GoogleGenAI } from "@google/genai";
import {
getTranslationFromHistory,
saveTranslationToHistory
} from "./mysql";

const ai = new GoogleGenAI({});

export type TranslationResult = {
original_message: string;
translated_message: string;
detected_language: string;
from_cache?: boolean;
};

export async function translate(
message: string,
language: string = "english"
): Promise<TranslationResult> {
// Check history first
const cachedTranslation = await getTranslationFromHistory(message, language);

if (cachedTranslation) {
console.log("✨ Translation found in cache");
return {
original_message: cachedTranslation.original_message,
translated_message: cachedTranslation.translated_message,
detected_language: cachedTranslation.detected_language || "unknown",
from_cache: true,
};
}

console.log("🔄 Requesting new translation from Gemini");

// Not in cache, use Gemini
// Sanitize inputs to prevent prompt injection
const sanitizedMessage = message.replace(/"/g, '\\"').slice(0, 2000); // Escape quotes and limit length
const sanitizedLanguage = language.replace(/[^a-zA-Z\s-]/g, '').slice(0, 50); // Allow only letters, spaces, hyphens
Expand Down Expand Up @@ -66,9 +87,20 @@ Do NOT include any extra text, commentary, markdown, or backticks.
}
}

return {
const result = {
original_message: jsonResponse.original_message ?? message,
translated_message: jsonResponse.translated_message ?? message,
detected_language: jsonResponse.detected_language ?? "unknown",
from_cache: false,
};

// Save to history for future use
await saveTranslationToHistory(
result.original_message,
language,
result.detected_language,
result.translated_message
);

return result;
}
13 changes: 9 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import nacl from "tweetnacl";
import { register } from "./register";
import { translate } from "./gemini";
import { initMySQLPool, initDatabase } from "./mysql";

// Debug logging helper
const DEBUG_MODE = process.env.DEBUG_MODE === "1";
Expand Down Expand Up @@ -33,6 +34,10 @@ function validateEnv() {

validateEnv();

// Initialize MySQL
initMySQLPool();
await initDatabase();

const PUB_KEY = process.env.PUBLIC_KEY!;
const DISCORD_TOKEN = process.env.DISCORD_TOKEN!;
await register();
Expand Down Expand Up @@ -118,8 +123,8 @@ const commandHandlers: Record<string, CommandHandler> = {
debug("Translation result:", translation);

const embed = {
title: "Translation",
color: 0x1abc9c,
title: translation.from_cache ? "Translation (from cache ✨)" : "Translation",
color: translation.from_cache ? 0x3498db : 0x1abc9c,
fields: [
{ name: "Original Message", value: translation.original_message || "N/A" },
{ name: "Detected Language", value: translation.detected_language || "N/A" },
Expand Down Expand Up @@ -175,8 +180,8 @@ const commandHandlers: Record<string, CommandHandler> = {
debug("Translation result:", translation);

const embed = {
title: `Translation (${language})`,
color: 0x1abc9c,
title: translation.from_cache ? `Translation (${language}) - from cache ✨` : `Translation (${language})`,
color: translation.from_cache ? 0x3498db : 0x1abc9c,
fields: [
{ name: "Original Message", value: translation.original_message || "N/A" },
{ name: "Detected Language", value: translation.detected_language || "N/A" },
Expand Down
Loading
Loading