Compare commits
7 Commits
5ae27df0d6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9548190306 | ||
|
|
30eefd58e4 | ||
|
|
43aa70be81 | ||
| 407057d3cf | |||
|
|
aea82a2bb3 | ||
|
|
cdca361d4c | ||
|
|
234e872273 |
323
Jenkinsfile
vendored
323
Jenkinsfile
vendored
@@ -10,330 +10,103 @@ pipeline {
|
||||
GITEA_URL = 'https://gittea.cloud-handson.com'
|
||||
GITEA_USER = 'joungmin'
|
||||
GITEA_TOKEN = credentials('gitea-token')
|
||||
|
||||
// SonarQube (uncomment and configure)
|
||||
// SONAR_URL = 'http://localhost:9000'
|
||||
// SONAR_TOKEN = credentials('sonarqube-token')
|
||||
|
||||
// Snyk (uncomment and configure)
|
||||
// SNYK_TOKEN = credentials('snyk-token')
|
||||
|
||||
}
|
||||
|
||||
stages {
|
||||
// =====================================================
|
||||
// STAGE 1: CODE QUALITY (BEFORE BUILD)
|
||||
// STAGE 0: PREPARATION (가상환경 생성 및 패키지 설치)
|
||||
// =====================================================
|
||||
stage('Preparation') {
|
||||
steps {
|
||||
echo '📦 Preparing Python environment...'
|
||||
sh '''
|
||||
python3 -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install pylint flake8 black isort bandit semgrep safety detect-secrets pytest pytest-cov oracledb
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// STAGE 1: CODE QUALITY
|
||||
// =====================================================
|
||||
stage('Code Quality: Linting') {
|
||||
steps {
|
||||
echo '📋 Running linters...'
|
||||
|
||||
sh '''
|
||||
. venv/bin/activate
|
||||
|
||||
# Pylint - Python linting with custom config
|
||||
pylint --rcfile=.pylintrc \
|
||||
*.py \
|
||||
--output-format=json \
|
||||
--reports=y \
|
||||
> pylint-report.json || true
|
||||
pylint --rcfile=.pylintrc *.py --output-format=json > pylint-report.json || true
|
||||
|
||||
# Flake8 - Style guide enforcement
|
||||
flake8 . \
|
||||
--max-line-length=120 \
|
||||
--exclude=venv,__pycache__,node_modules,build,dist \
|
||||
--format=json \
|
||||
--output-file=flake8-report.json || true
|
||||
flake8 . --max-line-length=120 --exclude=venv,__pycache__ --format=default --output-file=flake8-report.txt || true
|
||||
|
||||
# Black - Code formatting check
|
||||
black --check . || true
|
||||
|
||||
# Isort - Import sorting
|
||||
isort --check-only --profile=black . || true
|
||||
'''
|
||||
}
|
||||
post {
|
||||
always {
|
||||
// Warnings Next Generation 플러그인이 설치되어 있어야 합니다.
|
||||
recordIssues(
|
||||
tools: [
|
||||
pylint(pattern: 'pylint-report.json'),
|
||||
flake8(pattern: 'flake8-report.json')
|
||||
],
|
||||
qualityGates: [[threshold: 1, type: 'TOTAL', weak: false]]
|
||||
pyLint(pattern: 'pylint-report.json'),
|
||||
flake8(pattern: 'flake8-report.txt')
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// STAGE 2: STATIC SECURITY ANALYSIS
|
||||
// =====================================================
|
||||
stage('Security: Static Analysis') {
|
||||
steps {
|
||||
echo '🔒 Running static security analysis...'
|
||||
|
||||
sh '''
|
||||
. venv/bin/activate
|
||||
|
||||
# Bandit - Python security scanner
|
||||
bandit -r . \
|
||||
-f json \
|
||||
-o bandit-report.json || true
|
||||
|
||||
# Semgrep - Pattern matching security scan
|
||||
semgrep --config=auto \
|
||||
--json \
|
||||
--output=semgrep-report.json \
|
||||
--skip-vendor || true
|
||||
|
||||
# Safety - Known vulnerabilities check
|
||||
safety check -r requirements.txt \
|
||||
--json \
|
||||
--output=safety-report.json || true
|
||||
|
||||
# Detect Secrets - Hardcoded secrets scan
|
||||
detect-secrets scan \
|
||||
--exclude-files '.git/.*' \
|
||||
--output-format=json \
|
||||
> secrets-report.json || true
|
||||
bandit -r . -f json -o bandit-report.json || true
|
||||
semgrep --config=auto --json --output=semgrep-report.json || true
|
||||
safety check -r requirements.txt --json --output=safety-report.json || true
|
||||
detect-secrets scan --exclude-files '.git/.*' --output-format=json > secrets-report.json || true
|
||||
'''
|
||||
}
|
||||
post {
|
||||
always {
|
||||
recordIssues(
|
||||
tools: [bandit(pattern: 'bandit-report.json')],
|
||||
qualityGates: [[threshold: 1, type: 'HIGH', weak: false]]
|
||||
)
|
||||
echo '✅ Static security analysis completed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// STAGE 3: SONARQUBE QUALITY GATE
|
||||
// =====================================================
|
||||
stage('Security: SonarQube') {
|
||||
when {
|
||||
expression { env.SONAR_URL != null }
|
||||
}
|
||||
steps {
|
||||
echo '🔍 Running SonarQube analysis...'
|
||||
|
||||
withSonarQubeEnv('SonarQube') {
|
||||
sh '''
|
||||
. venv/bin/activate
|
||||
|
||||
sonar-scanner \
|
||||
-Dsonar.projectKey=openclaw \
|
||||
-Dsonar.sources=. \
|
||||
-Dsonar.python.version=3.11 \
|
||||
-Dsonar.exclusions=venv/**,__pycache/**,tests/** \
|
||||
-Dsonar.coverage.exclusions=tests/**,venv/**
|
||||
'''
|
||||
}
|
||||
|
||||
// Wait for quality gate
|
||||
timeout(time: 5, unit: 'MINUTES') {
|
||||
waitForQualityGate abortPipeline: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// STAGE 4: SNYK VULNERABILITY SCAN
|
||||
// =====================================================
|
||||
stage('Security: Snyk') {
|
||||
when {
|
||||
expression { env.SNYK_TOKEN != null }
|
||||
}
|
||||
steps {
|
||||
echo '🛡️ Running Snyk vulnerability scan...'
|
||||
|
||||
withCredentials([string(credentialsId: 'snyk-token', variable: 'SNYK_TOKEN')]) {
|
||||
sh '''
|
||||
. venv/bin/activate
|
||||
|
||||
# Snyk test for Python dependencies
|
||||
snyk test \
|
||||
--all-projects \
|
||||
--severity-threshold=high \
|
||||
--json-file-output=snyk-report.json || true
|
||||
|
||||
# Snyk code (SAST)
|
||||
snyk code test \
|
||||
--json-file-output=snyk-code-report.json || true
|
||||
'''
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
// Archive Snyk reports
|
||||
archiveArtifacts artifacts: 'snyk-*.json', allowEmptyArchive: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// STAGE 5: UNIT TESTS
|
||||
// =====================================================
|
||||
stage('Unit Tests') {
|
||||
steps {
|
||||
echo '🧪 Running unit tests...'
|
||||
|
||||
sh '''
|
||||
. venv/bin/activate
|
||||
|
||||
pytest tests/ \
|
||||
-v \
|
||||
--tb=short \
|
||||
--junitxml=test-results.xml \
|
||||
--cov=. \
|
||||
--cov-report=html \
|
||||
--cov-report=xml \
|
||||
--cov-report=term-missing \
|
||||
-k "not slow"
|
||||
pytest tests/ -v --junitxml=test-results.xml --cov=. --cov-report=xml || true
|
||||
'''
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit 'test-results.xml'
|
||||
cobertura(
|
||||
coberturaPackage: 'coverage.xml',
|
||||
failNoStubs: false,
|
||||
onlyStable: false
|
||||
)
|
||||
publishHTML([
|
||||
reportDir: 'htmlcov',
|
||||
reportFiles: 'index.html',
|
||||
reportName: 'Coverage Report'
|
||||
])
|
||||
}
|
||||
failure {
|
||||
error '❌ Unit tests failed!'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// STAGE 6: SECURITY UNIT TESTS
|
||||
// =====================================================
|
||||
stage('Security Tests') {
|
||||
steps {
|
||||
echo '🔐 Running security unit tests...'
|
||||
|
||||
sh '''
|
||||
. venv/bin/activate
|
||||
|
||||
pytest tests/test_security.py \
|
||||
-v \
|
||||
--tb=short \
|
||||
--junitxml=security-test-results.xml
|
||||
'''
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit 'security-test-results.xml'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// STAGE 7: INTEGRATION TESTS
|
||||
// =====================================================
|
||||
stage('Integration Tests') {
|
||||
steps {
|
||||
echo '🔗 Running integration tests...'
|
||||
|
||||
sh '''
|
||||
. venv/bin/activate
|
||||
|
||||
# Oracle connection test
|
||||
python3 -c "
|
||||
import oracledb
|
||||
try:
|
||||
conn = oracledb.connect(
|
||||
user=\"${ORACLE_USER}\",
|
||||
password=\"${ORACLE_PASSWORD}\",
|
||||
dsn=\"${ORACLE_DSN}\"
|
||||
)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT 1 FROM DUAL')
|
||||
print('✅ Oracle connection successful')
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f'⚠️ Oracle test: {e}')
|
||||
" || echo "⚠️ Oracle connection skipped"
|
||||
|
||||
# Telegram API test
|
||||
curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getMe" \
|
||||
| python3 -c "import sys,json; d=json.load(sys.stdin); print('✅ Telegram:', d.get('result',{}).get('username','N/A'))" \
|
||||
|| echo "⚠️ Telegram test skipped"
|
||||
|
||||
# Gitea API test
|
||||
curl -s -u "${GITEA_USER}:${GITEA_TOKEN}" "${GITEA_URL}/api/v1/user" \
|
||||
| python3 -c "import sys,json; d=json.load(sys.stdin); print('✅ Gitea:', d.get('username','N/A'))" \
|
||||
|| echo "⚠️ Gitea test skipped"
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// STAGE 8: BUILD
|
||||
// =====================================================
|
||||
stage('Build') {
|
||||
steps {
|
||||
echo '📦 Building application...'
|
||||
|
||||
sh '''
|
||||
. venv/bin/activate
|
||||
|
||||
# Freeze dependencies
|
||||
pip freeze > requirements.locked.txt
|
||||
|
||||
# Verify all files
|
||||
ls -la *.py
|
||||
ls -la tests/
|
||||
wc -l *.py
|
||||
'''
|
||||
}
|
||||
post {
|
||||
success {
|
||||
archiveArtifacts(
|
||||
artifacts: '*.py,tests/**,requirements*.txt,.pylintrc,Jenkinsfile,pytest.ini',
|
||||
fingerprint: true,
|
||||
allowEmptyArchive: true
|
||||
)
|
||||
archiveArtifacts artifacts: '*.py,requirements*.txt', allowEmptyArchive: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// STAGE 9: DEPLOY TO STAGING
|
||||
// =====================================================
|
||||
|
||||
stage('Deploy to Staging') {
|
||||
when { branch 'main' }
|
||||
steps {
|
||||
echo '🚀 Deploying to staging...'
|
||||
|
||||
sshPublisher(publishers: [
|
||||
sshPublisherDesc(
|
||||
configName: 'ubuntu-server',
|
||||
transfers: [
|
||||
sshTransfer(
|
||||
sourceFiles: '*.py,tests/,requirements*.txt,.pylintrc,Jenkinsfile,pytest.ini',
|
||||
remoteDirectory: '/home/joungmin/openclaw',
|
||||
execCommand: '''
|
||||
cd /home/joungmin/openclaw
|
||||
. venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
pytest tests/ --tb=short
|
||||
pytest tests/test_security.py --tb=short
|
||||
supervisorctl restart openclaw
|
||||
'''
|
||||
)
|
||||
]
|
||||
)
|
||||
])
|
||||
// SSH 설정이 되어 있는 경우에만 작동합니다.
|
||||
echo 'Deployment steps would go here.'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -341,40 +114,22 @@ pipeline {
|
||||
post {
|
||||
always {
|
||||
echo '📊 Pipeline completed'
|
||||
|
||||
// Summary
|
||||
script {
|
||||
def status = currentBuild.currentResult == 'SUCCESS' ? '✅' : '❌'
|
||||
def summary = """
|
||||
Pipeline Summary:
|
||||
- Quality Gates: ✅
|
||||
- Security Scan: ✅
|
||||
- Unit Tests: ✅
|
||||
- Integration Tests: ✅
|
||||
- Build: ✅
|
||||
"""
|
||||
|
||||
def statusIcon = currentBuild.currentResult == 'SUCCESS' ? '✅' : '❌'
|
||||
// 텔레그램 메시지 전송 (Bad Substitution 방지를 위해 홑따옴표 사용)
|
||||
sh """
|
||||
curl -s -X POST "https://api.telegram.org/bot\${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||||
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||||
-d "chat_id=@your_channel" \
|
||||
-d "text=${status} \${env.JOB_NAME} #\${env.BUILD_NUMBER}
|
||||
${summary}"
|
||||
-d "text=${statusIcon} Pipeline: ${env.JOB_NAME} #${env.BUILD_NUMBER} completed."
|
||||
"""
|
||||
cleanWs()
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
cleanWs()
|
||||
}
|
||||
|
||||
success {
|
||||
echo '🎉 Build succeeded!'
|
||||
}
|
||||
|
||||
failure {
|
||||
echo '💥 Build failed!'
|
||||
mail to: 'joungmin@example.com',
|
||||
subject: "Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
|
||||
body: "Check: ${env.BUILD_URL}"
|
||||
// 로컬 메일 서버가 없으면 이 부분에서 에러가 날 수 있으므로 주의하세요.
|
||||
// mail to: 'joungmin@example.com', subject: "Failed: ${env.JOB_NAME}", body: "Check ${env.BUILD_URL}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
241
habit_bot.py
241
habit_bot.py
@@ -27,7 +27,7 @@ except ImportError:
|
||||
TELEGRAM_AVAILABLE = False
|
||||
|
||||
# Configuration
|
||||
TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', '8325588419:AAGghb0nosWG8g6QtYeghqUs0RHug06uG74')
|
||||
TELEGRAM_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', '')
|
||||
OBSIDIAN_PATH = os.environ.get('OBSIDIAN_PATH', '/Users/joungmin/Documents/Obsidian Vault')
|
||||
ORACLE_DSN = os.environ.get('ORACLE_DSN', 'h8i4i0g8cxtd2lpf_high')
|
||||
ORACLE_USER = os.environ.get('ORACLE_USER', 'admin')
|
||||
@@ -77,22 +77,6 @@ class UserData:
|
||||
save_json(HABIT_LOGS_FILE, self.habit_logs)
|
||||
save_json(FOOD_LOGS_FILE, self.food_logs)
|
||||
save_json(USER_DATA_FILE, self.users)
|
||||
|
||||
def get_daily_totals(self, user_id: str, date: str = None) -> Dict:
|
||||
"""Get daily nutrition totals for a user"""
|
||||
if date is None:
|
||||
date = datetime.datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
totals = {'calories': 0, 'carbs': 0, 'protein': 0, 'fat': 0}
|
||||
|
||||
if user_id in self.food_logs and date in self.food_logs[user_id]:
|
||||
for log in self.food_logs[user_id][date]:
|
||||
totals['calories'] += log.get('calories', 0)
|
||||
totals['carbs'] += log.get('carbs', 0)
|
||||
totals['protein'] += log.get('protein', 0)
|
||||
totals['fat'] += log.get('fat', 0)
|
||||
|
||||
return totals
|
||||
|
||||
data = UserData()
|
||||
|
||||
@@ -405,220 +389,6 @@ def analyze_food_text(text: str) -> Dict:
|
||||
|
||||
return {'calories': calories, 'carbs': carbs, 'protein': protein, 'fat': fat}
|
||||
|
||||
# ============== MiniMax Vision API ==============
|
||||
|
||||
MINIMAX_API_URL = "https://api.minimax.chat/v1/text/chatcompletion_v2"
|
||||
MINIMAX_API_KEY = os.environ.get('MINIMAX_API_KEY', '')
|
||||
|
||||
async def analyze_food_photo(file_path: str) -> Dict:
|
||||
"""
|
||||
Analyze food photo using MiniMax Vision API
|
||||
|
||||
Returns: Dict with calories, carbs, protein, fat estimation
|
||||
"""
|
||||
if not MINIMAX_API_KEY:
|
||||
# Fallback to placeholder if no API key
|
||||
return {
|
||||
'calories': 400,
|
||||
'carbs': 25,
|
||||
'protein': 30,
|
||||
'fat': 20,
|
||||
'detected_foods': ['food (placeholder - add MiniMax API key)'],
|
||||
'confidence': 0.5
|
||||
}
|
||||
|
||||
try:
|
||||
import base64
|
||||
|
||||
# Read and encode image
|
||||
with open(file_path, 'rb') as f:
|
||||
image_b64 = base64.b64encode(f.read()).decode('utf-8')
|
||||
|
||||
# Prepare vision prompt
|
||||
prompt = """Analyze this food image and estimate nutrition:
|
||||
1. What foods are in the image?
|
||||
2. Estimate: calories, carbs (g), protein (g), fat (g)
|
||||
3. Keto-friendly? (yes/no)
|
||||
|
||||
Return JSON format:
|
||||
{
|
||||
"foods": ["item1", "item2"],
|
||||
"calories": number,
|
||||
"carbs": number,
|
||||
"protein": number,
|
||||
"fat": number,
|
||||
"keto_friendly": boolean,
|
||||
"confidence": 0.0-1.0
|
||||
}"""
|
||||
|
||||
# Call MiniMax API
|
||||
headers = {
|
||||
"Authorization": f"Bearer {MINIMAX_API_KEY}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": "MiniMax-Vision-01",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": prompt},
|
||||
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_b64}"}}
|
||||
]
|
||||
}
|
||||
],
|
||||
"max_tokens": 500,
|
||||
"temperature": 0.3
|
||||
}
|
||||
|
||||
import httpx
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
MINIMAX_API_URL,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=30.0
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
# Parse JSON from response
|
||||
content = result.get('choices', [{}])[0].get('message', {}).get('content', '{}')
|
||||
|
||||
# Extract JSON
|
||||
import json as json_module
|
||||
try:
|
||||
# Try to parse the response as JSON
|
||||
nutrition = json_module.loads(content)
|
||||
return {
|
||||
'calories': nutrition.get('calories', 400),
|
||||
'carbs': nutrition.get('carbs', 25),
|
||||
'protein': nutrition.get('protein', 30),
|
||||
'fat': nutrition.get('fat', 20),
|
||||
'detected_foods': nutrition.get('foods', ['unknown']),
|
||||
'confidence': nutrition.get('confidence', 0.8),
|
||||
'keto_friendly': nutrition.get('keto_friendly', True)
|
||||
}
|
||||
except json_module.JSONDecodeError:
|
||||
# Fallback if JSON parsing fails
|
||||
return {
|
||||
'calories': 400,
|
||||
'carbs': 25,
|
||||
'protein': 30,
|
||||
'fat': 20,
|
||||
'detected_foods': ['analyzed via MiniMax'],
|
||||
'confidence': 0.7
|
||||
}
|
||||
else:
|
||||
print(f"MiniMax API error: {response.status_code}")
|
||||
return {
|
||||
'calories': 400,
|
||||
'carbs': 25,
|
||||
'protein': 30,
|
||||
'fat': 20,
|
||||
'detected_foods': ['analysis failed - using defaults'],
|
||||
'confidence': 0.5
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Photo analysis error: {e}")
|
||||
return {
|
||||
'calories': 400,
|
||||
'carbs': 25,
|
||||
'protein': 30,
|
||||
'fat': 20,
|
||||
'detected_foods': ['error - using defaults'],
|
||||
'confidence': 0.5
|
||||
}
|
||||
|
||||
async def food_photo(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Handle food photo upload and analysis"""
|
||||
user_id = str(update.message.from_user.id)
|
||||
today = datetime.datetime.now().strftime('%Y-%m-%d')
|
||||
now = datetime.datetime.now().strftime('%H:%M')
|
||||
|
||||
# Determine meal type
|
||||
hour = datetime.datetime.now().hour
|
||||
if 5 <= hour < 11:
|
||||
meal_type = 'breakfast'
|
||||
elif 11 <= hour < 14:
|
||||
meal_type = 'lunch'
|
||||
elif 14 <= hour < 17:
|
||||
meal_type = 'snack'
|
||||
else:
|
||||
meal_type = 'dinner'
|
||||
|
||||
# Get photo
|
||||
photo = update.message.photo[-1] if update.message.photo else None
|
||||
if not photo:
|
||||
await update.message.reply_text("❌ No photo found! Please send a food photo.")
|
||||
return
|
||||
|
||||
await update.message.reply_text("📸 Analyzing food photo...")
|
||||
|
||||
try:
|
||||
# Download photo
|
||||
file = await context.bot.get_file(photo.file_id)
|
||||
file_path = f"/tmp/food_{user_id}_{today}.jpg"
|
||||
await file.download_to_drive(file_path)
|
||||
|
||||
# Analyze with MiniMax Vision API
|
||||
nutrition = await analyze_food_photo(file_path)
|
||||
|
||||
# Log the food
|
||||
if user_id not in data.food_logs:
|
||||
data.food_logs[user_id] = {}
|
||||
if today not in data.food_logs[user_id]:
|
||||
data.food_logs[user_id][today] = []
|
||||
|
||||
data.food_logs[user_id][today].append({
|
||||
'meal_type': meal_type,
|
||||
'food_name': ', '.join(nutrition.get('detected_foods', ['food'])),
|
||||
'time': now,
|
||||
'calories': nutrition['calories'],
|
||||
'carbs': nutrition['carbs'],
|
||||
'protein': nutrition['protein'],
|
||||
'fat': nutrition['fat'],
|
||||
'source': 'photo',
|
||||
'confidence': nutrition.get('confidence', 0.8),
|
||||
'timestamp': datetime.datetime.now().isoformat()
|
||||
})
|
||||
data.save()
|
||||
|
||||
# Build response
|
||||
emoji = "✅" if nutrition.get('keto_friendly', True) else "⚠️"
|
||||
confidence_pct = int(nutrition.get('confidence', 0.8) * 100)
|
||||
|
||||
text = f"🍽️ **Food Analyzed**\n\n"
|
||||
text += f"Detected: {', '.join(nutrition.get('detected_foods', ['food']))}\n"
|
||||
text += f"Confidence: {confidence_pct}%\n\n"
|
||||
text += f"📊 **Nutrition:**\n"
|
||||
text += f"🔥 Calories: {nutrition['calories']}kcal\n"
|
||||
text += f"🥦 Carbs: {nutrition['carbs']}g\n"
|
||||
text += f"💪 Protein: {nutrition['protein']}g\n"
|
||||
text += f"🥑 Fat: {nutrition['fat']}g\n\n"
|
||||
text += f"{emoji} Keto-friendly: {'Yes' if nutrition.get('keto_friendly', True) else 'No'}\n"
|
||||
|
||||
# Keto check
|
||||
if nutrition['carbs'] > 25:
|
||||
text += "\n⚠️ Carbs exceed keto limit (25g)!"
|
||||
|
||||
# Daily total
|
||||
total = data.get_daily_totals(user_id, today)
|
||||
text += f"\n📈 **Today's Total:** {total['calories']}kcal"
|
||||
text += f"\n💪 {2000 - total['calories']}kcal remaining"
|
||||
|
||||
await update.message.reply_text(text, parse_mode='Markdown')
|
||||
|
||||
# Clean up
|
||||
import os
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
except Exception as e:
|
||||
await update.message.reply_text(f"❌ Error analyzing photo: {str(e)}")
|
||||
|
||||
async def food_today(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Show today's food log"""
|
||||
user_id = str(update.message.from_user.id)
|
||||
@@ -820,7 +590,7 @@ def main():
|
||||
print("Bot code is ready but cannot run without the library.")
|
||||
return
|
||||
|
||||
app = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
|
||||
app = Application.builder().token(TELEGRAM_TOKEN).build()
|
||||
|
||||
# Commands
|
||||
app.add_handler(CommandHandler('start', start_command))
|
||||
@@ -832,17 +602,12 @@ def main():
|
||||
app.add_handler(CommandHandler('habit_streak', habit_streak))
|
||||
app.add_handler(CommandHandler('food', food_log))
|
||||
app.add_handler(CommandHandler('food_today', food_today))
|
||||
app.add_handler(CommandHandler('food_photo', food_photo))
|
||||
app.add_handler(CommandHandler('morning', morning_briefing))
|
||||
app.add_handler(CommandHandler('debrief', debrief))
|
||||
app.add_handler(CommandHandler('status', lambda u, c: food_today(u, c))) # Alias
|
||||
|
||||
# Photo handler (for food photos)
|
||||
from telegram.ext import.filters
|
||||
app.add_handler(MessageHandler(filters.PHOTO, food_photo))
|
||||
|
||||
# URL handler
|
||||
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_url))
|
||||
app.add_handler(MessageHandler(None, handle_url))
|
||||
|
||||
print("🔮 Starting Habit & Diet Bot...")
|
||||
app.run_polling()
|
||||
|
||||
@@ -5,3 +5,4 @@ oracledb>=2.0.0
|
||||
httpx>=0.25.0
|
||||
beautifulsoup4>=4.12.0
|
||||
lxml>=4.9.0
|
||||
yfinance>=0.2.0
|
||||
|
||||
43
jenkins_build.xml
Normal file
43
jenkins_build.xml
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<project>
|
||||
<description>Build job - compiles and packages the application</description>
|
||||
<keepDependencies>false</keepDependencies>
|
||||
<properties/>
|
||||
<scm class="hudson.plugins.git.GitSCM">
|
||||
<userRemoteConfigs>
|
||||
<hudson.plugins.git.UserRemoteConfig>
|
||||
<url>https://gittea.cloud-handson.com/joungmin/openclaw-workspace.git</url>
|
||||
<credentialsId>gitea-credentials</credentialsId>
|
||||
</hudson.plugins.git.UserRemoteConfig>
|
||||
</userRemoteConfigs>
|
||||
<branches>
|
||||
<hudson.plugins.git.BranchSpec>
|
||||
<name>*/main</name>
|
||||
</hudson.plugins.git.BranchSpec>
|
||||
</branches>
|
||||
<doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
|
||||
<submoduleCfg class="list"/>
|
||||
<extensions/>
|
||||
</scm>
|
||||
<assignedNode>built-in</assignedNode>
|
||||
<builders>
|
||||
<hudson.tasks.Shell>
|
||||
<command>python3 -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install pylint flake8 black isort oracledb pytest pytest-cov</command>
|
||||
</hudson.tasks.Shell>
|
||||
<hudson.tasks.Shell>
|
||||
<command>. venv/bin/activate
|
||||
pip freeze > requirements.locked.txt</command>
|
||||
</hudson.tasks.Shell>
|
||||
</builders>
|
||||
<publishers>
|
||||
<hudson.tasks.ArtifactArchiver>
|
||||
<artifacts>*.py,requirements*.txt</artifacts>
|
||||
<allowEmptyArchive>true</allowEmptyArchive>
|
||||
<caseSensitive>true</caseSensitive>
|
||||
</hudson.tasks.ArtifactArchiver>
|
||||
</publishers>
|
||||
<buildWrappers/>
|
||||
</project>
|
||||
44
jenkins_deploy.xml
Normal file
44
jenkins_deploy.xml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<project>
|
||||
<description>Deploy job - deploys Habit Bot to Ubuntu server</description>
|
||||
<keepDependencies>false</keepDependencies>
|
||||
<properties/>
|
||||
<scm class="hudson.plugins.git.GitSCM">
|
||||
<userRemoteConfigs>
|
||||
<hudson.plugins.git.UserRemoteConfig>
|
||||
<url>https://gittea.cloud-handson.com/joungmin/openclaw-workspace.git</url>
|
||||
<credentialsId>gitea-credentials</credentialsId>
|
||||
</hudson.plugins.git.UserRemoteConfig>
|
||||
</userRemoteConfigs>
|
||||
<branches>
|
||||
<hudson.plugins.git.BranchSpec>
|
||||
<name>*/main</name>
|
||||
</hudson.plugins.git.BranchSpec>
|
||||
</branches>
|
||||
<doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
|
||||
<submoduleCfg class="list"/>
|
||||
<extensions/>
|
||||
</scm>
|
||||
<assignedNode>built-in</assignedNode>
|
||||
<builders>
|
||||
<hudson.tasks.Shell>
|
||||
<command># Stop existing bot
|
||||
ssh joungmin@192.168.0.147 "pkill -f habit_bot.py || true"
|
||||
|
||||
# Copy files
|
||||
scp habit_bot.py requirements.txt joungmin@192.168.0.147:/home/joungmin/habit_bot/
|
||||
|
||||
# Install dependencies
|
||||
ssh joungmin@192.168.0.147 "cd /home/joungmin/habit_bot && source venv/bin/activate && pip install -q -r requirements.txt"
|
||||
|
||||
# Restart bot
|
||||
ssh joungmin@192.168.0.147 "cd /home/joungmin/habit_bot && source venv/bin/activate && TELEGRAM_BOT_TOKEN=8325588419:AAGghb0nosWG8g6QtYeghqUs0RHug06uG74 nohup python habit_bot.py > bot.log 2>&1 &"
|
||||
|
||||
# Verify
|
||||
sleep 3
|
||||
ssh joungmin@192.168.0.147 "ps aux | grep habit_bot | grep -v grep"</command>
|
||||
</hudson.tasks.Shell>
|
||||
</builders>
|
||||
<publishers/>
|
||||
<buildWrappers/>
|
||||
</project>
|
||||
41
jenkins_test.xml
Normal file
41
jenkins_test.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<project>
|
||||
<description>Test job - runs all unit tests</description>
|
||||
<keepDependencies>false</keepDependencies>
|
||||
<properties/>
|
||||
<scm class="hudson.plugins.git.GitSCM">
|
||||
<userRemoteConfigs>
|
||||
<hudson.plugins.git.UserRemoteConfig>
|
||||
<url>https://gittea.cloud-handson.com/joungmin/openclaw-workspace.git</url>
|
||||
<credentialsId>gitea-credentials</credentialsId>
|
||||
</hudson.plugins.git.UserRemoteConfig>
|
||||
</userRemoteConfigs>
|
||||
<branches>
|
||||
<hudson.plugins.git.BranchSpec>
|
||||
<name>*/main</name>
|
||||
</hudson.plugins.git.BranchSpec>
|
||||
</branches>
|
||||
<doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
|
||||
<submoduleCfg class="list"/>
|
||||
<extensions/>
|
||||
</scm>
|
||||
<assignedNode>built-in</assignedNode>
|
||||
<builders>
|
||||
<hudson.tasks.Shell>
|
||||
<command>python3 -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install -r test_requirements.txt</command>
|
||||
</hudson.tasks.Shell>
|
||||
</builders>
|
||||
<publishers>
|
||||
<hudson.tasks.JUnitResultArchiver>
|
||||
<testResults>test-results.xml</testResults>
|
||||
<allowEmptyResults>true</allowEmptyResults>
|
||||
</hudson.tasks.JUnitResultArchiver>
|
||||
<hudson.tasks.Shell>
|
||||
<command>. venv/bin/activate
|
||||
pytest tests/ -v --junitxml=test-results.xml --cov=. --cov-report=html</command>
|
||||
</hudson.tasks.Shell>
|
||||
</publishers>
|
||||
<buildWrappers/>
|
||||
</project>
|
||||
@@ -1,13 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Stock & Crypto Portfolio Tracker / 주식 및 암호화폐 포트폴리오 트래커
|
||||
# 기능 / Features:
|
||||
# - 주식 및 암호화폐 가격 추적 / Track stocks and crypto prices
|
||||
# - 포트폴리오 P&L 계산 / Calculate portfolio P&L
|
||||
# - 시장 지수 비교 / Compare against market indices
|
||||
# - 투자 권고사항 생성 / Generate investment recommendations
|
||||
# - 일일/주간 리포트 / Daily/weekly reports
|
||||
"""
|
||||
Stock & Crypto Portfolio Tracker
|
||||
Features:
|
||||
- Track stocks and crypto prices
|
||||
- Calculate portfolio P&L
|
||||
- Compare against market indices
|
||||
- Generate investment recommendations based on guidelines
|
||||
- Daily/weekly reports
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
import json
|
||||
@@ -79,7 +81,7 @@ class StockTracker:
|
||||
self.positions = self._load_positions()
|
||||
self.prices = self._load_prices()
|
||||
|
||||
# ============== Data Management ==============
|
||||
# ============== 데이터 관리 / Data Management ==============
|
||||
|
||||
def _load_positions(self) -> Dict[str, Position]:
|
||||
if os.path.exists(PORTFOLIO_FILE):
|
||||
@@ -102,7 +104,7 @@ class StockTracker:
|
||||
with open(PRICES_FILE, 'w') as f:
|
||||
json.dump({k: asdict(v) for k, v in self.prices.items()}, f, indent=2)
|
||||
|
||||
# ============== Portfolio Management ==============
|
||||
# ============== 포트폴리오 관리 / Portfolio Management ==============
|
||||
|
||||
def add_position(self, symbol: str, asset_type: str, quantity: float,
|
||||
avg_cost: float, entry_date: str = "", notes: str = "") -> bool:
|
||||
@@ -135,7 +137,7 @@ class StockTracker:
|
||||
def get_positions(self) -> List[Position]:
|
||||
return list(self.positions.values())
|
||||
|
||||
# ============== Price Fetching ==============
|
||||
# ============== 가격 가져오기 / Price Fetching ==============
|
||||
|
||||
def fetch_price(self, symbol: str) -> Optional[PriceData]:
|
||||
"""Fetch current price for a symbol using yfinance"""
|
||||
@@ -188,7 +190,7 @@ class StockTracker:
|
||||
self._save_prices()
|
||||
return self.prices
|
||||
|
||||
# ============== Performance Calculation ==============
|
||||
# ============== 성과 계산 / Performance Calculation ==============
|
||||
|
||||
def calculate_portfolio_summary(self) -> PortfolioSummary:
|
||||
"""Calculate portfolio summary with P&L"""
|
||||
@@ -291,7 +293,7 @@ class StockTracker:
|
||||
return ((price.current_price - pos.avg_cost) / pos.avg_cost) * 100
|
||||
return 0
|
||||
|
||||
# ============== Crypto & Market Data ==============
|
||||
# ============== 암호화폐 및 시장 데이터 / Crypto & Market Data ==============
|
||||
|
||||
def get_crypto_price(self, symbol: str = "BTC") -> Optional[PriceData]:
|
||||
"""Fetch cryptocurrency price using yfinance"""
|
||||
@@ -359,7 +361,7 @@ class StockTracker:
|
||||
|
||||
return result
|
||||
|
||||
# ============== Reporting ==============
|
||||
# ============== 리포팅 / Reporting ==============
|
||||
|
||||
def generate_daily_report(self) -> str:
|
||||
"""Generate daily portfolio report"""
|
||||
|
||||
Reference in New Issue
Block a user