@cachly-dev/mcp-server 0.10.64 → 0.10.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/handlers/brain.js +1 -1
- package/dist/src/index.js +1 -1
- package/package.json +7 -1
- package/server.json +2 -2
- package/.env.test.example +0 -23
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
- package/.github/workflows/eas-build-android.yml +0 -58
- package/.github/workflows/eas-update.yml +0 -55
- package/.github/workflows/publish.yml +0 -72
- package/BENCH.md +0 -91
- package/CHANGELOG.md +0 -604
- package/CHANGELOG.md.tmp +0 -0
- package/Dockerfile +0 -16
- package/LAUNCH_TEXTS.md +0 -182
- package/PROGRESS.md +0 -137
- package/STRATEGY.md +0 -228
- package/VISION_10X.md +0 -133
- package/apps/travel-chaos-organizer/.env.example +0 -49
- package/apps/travel-chaos-organizer/DEPLOY.md +0 -328
- package/apps/travel-chaos-organizer/Makefile +0 -57
- package/apps/travel-chaos-organizer/README.md +0 -159
- package/apps/travel-chaos-organizer/backend/.env.example +0 -10
- package/apps/travel-chaos-organizer/backend/Dockerfile +0 -25
- package/apps/travel-chaos-organizer/backend/alembic/env.py +0 -56
- package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +0 -24
- package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +0 -24
- package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +0 -24
- package/apps/travel-chaos-organizer/backend/alembic.ini +0 -39
- package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +0 -81
- package/apps/travel-chaos-organizer/backend/app/config.py +0 -66
- package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/db/database.py +0 -20
- package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +0 -71
- package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +0 -31
- package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +0 -4
- package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +0 -5
- package/apps/travel-chaos-organizer/backend/app/limiter.py +0 -4
- package/apps/travel-chaos-organizer/backend/app/logging_config.py +0 -25
- package/apps/travel-chaos-organizer/backend/app/main.py +0 -77
- package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/models/schemas.py +0 -142
- package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/routers/admin.py +0 -237
- package/apps/travel-chaos-organizer/backend/app/routers/cache.py +0 -13
- package/apps/travel-chaos-organizer/backend/app/routers/events.py +0 -29
- package/apps/travel-chaos-organizer/backend/app/routers/export.py +0 -122
- package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +0 -122
- package/apps/travel-chaos-organizer/backend/app/routers/items.py +0 -130
- package/apps/travel-chaos-organizer/backend/app/routers/mail.py +0 -108
- package/apps/travel-chaos-organizer/backend/app/routers/parse.py +0 -303
- package/apps/travel-chaos-organizer/backend/app/routers/payments.py +0 -179
- package/apps/travel-chaos-organizer/backend/app/routers/trips.py +0 -134
- package/apps/travel-chaos-organizer/backend/app/routers/users.py +0 -169
- package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +0 -37
- package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/services/cache.py +0 -131
- package/apps/travel-chaos-organizer/backend/app/services/drip.py +0 -128
- package/apps/travel-chaos-organizer/backend/app/services/email.py +0 -61
- package/apps/travel-chaos-organizer/backend/app/services/errors.py +0 -44
- package/apps/travel-chaos-organizer/backend/app/services/notifier.py +0 -10
- package/apps/travel-chaos-organizer/backend/app/services/ollama.py +0 -101
- package/apps/travel-chaos-organizer/backend/app/services/parser.py +0 -22
- package/apps/travel-chaos-organizer/backend/app/services/quota.py +0 -85
- package/apps/travel-chaos-organizer/backend/app/services/sentry.py +0 -11
- package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +0 -35
- package/apps/travel-chaos-organizer/backend/pytest.ini +0 -4
- package/apps/travel-chaos-organizer/backend/requirements-dev.txt +0 -6
- package/apps/travel-chaos-organizer/backend/requirements.txt +0 -25
- package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/tests/conftest.py +0 -223
- package/apps/travel-chaos-organizer/backend/tests/test_admin.py +0 -99
- package/apps/travel-chaos-organizer/backend/tests/test_cache.py +0 -142
- package/apps/travel-chaos-organizer/backend/tests/test_drip.py +0 -32
- package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +0 -121
- package/apps/travel-chaos-organizer/backend/tests/test_export.py +0 -48
- package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +0 -107
- package/apps/travel-chaos-organizer/backend/tests/test_items.py +0 -138
- package/apps/travel-chaos-organizer/backend/tests/test_mail.py +0 -113
- package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +0 -145
- package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +0 -98
- package/apps/travel-chaos-organizer/backend/tests/test_parser.py +0 -72
- package/apps/travel-chaos-organizer/backend/tests/test_payments.py +0 -163
- package/apps/travel-chaos-organizer/backend/tests/test_quota.py +0 -46
- package/apps/travel-chaos-organizer/backend/tests/test_search.py +0 -67
- package/apps/travel-chaos-organizer/backend/tests/test_trips.py +0 -123
- package/apps/travel-chaos-organizer/backend/tests/test_users.py +0 -52
- package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/caddy/Caddyfile +0 -27
- package/apps/travel-chaos-organizer/docker-compose.node1.yml +0 -31
- package/apps/travel-chaos-organizer/docker-compose.override.yml +0 -14
- package/apps/travel-chaos-organizer/docker-compose.yml +0 -128
- package/apps/travel-chaos-organizer/frontend/.env.example +0 -13
- package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +0 -80
- package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +0 -166
- package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +0 -130
- package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +0 -381
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +0 -346
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +0 -363
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +0 -34
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +0 -35
- package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +0 -91
- package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +0 -71
- package/apps/travel-chaos-organizer/frontend/app/auth.tsx +0 -29
- package/apps/travel-chaos-organizer/frontend/app/index.tsx +0 -16
- package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +0 -119
- package/apps/travel-chaos-organizer/frontend/app.config.ts +0 -54
- package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +0 -66
- package/apps/travel-chaos-organizer/frontend/assets/icon.svg +0 -79
- package/apps/travel-chaos-organizer/frontend/assets/splash.svg +0 -15
- package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +0 -102
- package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +0 -107
- package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +0 -109
- package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +0 -45
- package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +0 -19
- package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +0 -155
- package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +0 -55
- package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +0 -80
- package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +0 -39
- package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +0 -39
- package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +0 -70
- package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +0 -121
- package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +0 -70
- package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +0 -73
- package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +0 -130
- package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +0 -60
- package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +0 -42
- package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +0 -146
- package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +0 -70
- package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +0 -44
- package/apps/travel-chaos-organizer/frontend/eas.json +0 -22
- package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +0 -24
- package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +0 -60
- package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +0 -112
- package/apps/travel-chaos-organizer/frontend/lib/api.ts +0 -223
- package/apps/travel-chaos-organizer/frontend/lib/auth.ts +0 -93
- package/apps/travel-chaos-organizer/frontend/lib/db.ts +0 -86
- package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +0 -54
- package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +0 -13
- package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +0 -112
- package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +0 -145
- package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +0 -43
- package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +0 -28
- package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +0 -44
- package/apps/travel-chaos-organizer/frontend/package-lock.json +0 -13221
- package/apps/travel-chaos-organizer/frontend/package.json +0 -44
- package/apps/travel-chaos-organizer/frontend/tsconfig.json +0 -23
- package/apps/travel-chaos-organizer/keycloak/realm-export.json +0 -59
- package/apps/travel-chaos-organizer/landing/index.html +0 -773
- package/apps/travel-chaos-organizer/scripts/backup.sh +0 -14
- package/apps/travel-chaos-organizer/scripts/configure-env.sh +0 -82
- package/apps/travel-chaos-organizer/scripts/deploy.sh +0 -54
- package/apps/travel-chaos-organizer/scripts/health-check.sh +0 -36
- package/apps/travel-chaos-organizer/scripts/migrate.sh +0 -7
- package/apps/travel-chaos-organizer/scripts/server-setup.sh +0 -85
- package/apps/travel-chaos-organizer/scripts/smoke-test.sh +0 -29
- package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +0 -71
- package/apps/travel-chaos-organizer/telegram-bot/.env.example +0 -3
- package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +0 -6
- package/apps/travel-chaos-organizer/telegram-bot/bot.py +0 -504
- package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +0 -3
- package/dist/src/__tests__/auth.test.d.ts +0 -1
- package/dist/src/__tests__/auth.test.js +0 -107
- package/dist/src/__tests__/auth.test.js.map +0 -1
- package/dist/src/__tests__/brain-flow.test.d.ts +0 -12
- package/dist/src/__tests__/brain-flow.test.js +0 -1241
- package/dist/src/__tests__/brain-flow.test.js.map +0 -1
- package/dist/src/__tests__/confidence.test.d.ts +0 -13
- package/dist/src/__tests__/confidence.test.js +0 -233
- package/dist/src/__tests__/confidence.test.js.map +0 -1
- package/dist/src/__tests__/e2e/api-auth.test.d.ts +0 -7
- package/dist/src/__tests__/e2e/api-auth.test.js +0 -162
- package/dist/src/__tests__/e2e/api-auth.test.js.map +0 -1
- package/dist/src/__tests__/e2e/api-health.test.d.ts +0 -7
- package/dist/src/__tests__/e2e/api-health.test.js +0 -95
- package/dist/src/__tests__/e2e/api-health.test.js.map +0 -1
- package/dist/src/__tests__/e2e/cli.test.d.ts +0 -10
- package/dist/src/__tests__/e2e/cli.test.js +0 -204
- package/dist/src/__tests__/e2e/cli.test.js.map +0 -1
- package/dist/src/__tests__/e2e/config.d.ts +0 -35
- package/dist/src/__tests__/e2e/config.js +0 -53
- package/dist/src/__tests__/e2e/config.js.map +0 -1
- package/dist/src/__tests__/e2e/funnel.test.d.ts +0 -13
- package/dist/src/__tests__/e2e/funnel.test.js +0 -90
- package/dist/src/__tests__/e2e/funnel.test.js.map +0 -1
- package/dist/src/__tests__/e2e/mcp-tools.test.d.ts +0 -7
- package/dist/src/__tests__/e2e/mcp-tools.test.js +0 -129
- package/dist/src/__tests__/e2e/mcp-tools.test.js.map +0 -1
- package/dist/src/__tests__/e2e/provisioning.test.d.ts +0 -15
- package/dist/src/__tests__/e2e/provisioning.test.js +0 -123
- package/dist/src/__tests__/e2e/provisioning.test.js.map +0 -1
- package/dist/src/__tests__/handlers-unit.test.d.ts +0 -12
- package/dist/src/__tests__/handlers-unit.test.js +0 -505
- package/dist/src/__tests__/handlers-unit.test.js.map +0 -1
- package/dist/src/__tests__/namespace.test.d.ts +0 -1
- package/dist/src/__tests__/namespace.test.js +0 -114
- package/dist/src/__tests__/namespace.test.js.map +0 -1
- package/dist/src/__tests__/rerank.test.d.ts +0 -1
- package/dist/src/__tests__/rerank.test.js +0 -115
- package/dist/src/__tests__/rerank.test.js.map +0 -1
- package/dist/src/__tests__/utils.test.d.ts +0 -1
- package/dist/src/__tests__/utils.test.js +0 -83
- package/dist/src/__tests__/utils.test.js.map +0 -1
- package/dist/src/bench/cachly-bench.d.ts +0 -35
- package/dist/src/bench/cachly-bench.js +0 -182
- package/dist/src/bench/cachly-bench.js.map +0 -1
- package/dist/src/bench/fixtures.d.ts +0 -37
- package/dist/src/bench/fixtures.js +0 -225
- package/dist/src/bench/fixtures.js.map +0 -1
- package/dist/src/search.test.d.ts +0 -1
- package/dist/src/search.test.js +0 -713
- package/dist/src/search.test.js.map +0 -1
- package/eslint.config.js +0 -16
- package/glama.json +0 -36
- package/llms.txt +0 -155
- package/packages/telegram-notify/SPLIT_PLAN.md +0 -106
- package/packages/telegram-notify/client.ts +0 -88
- package/packages/telegram-notify/emojis.json +0 -22
- package/packages/telegram-notify/pyproject.toml +0 -17
- package/packages/telegram-notify/telegram_notify/__init__.py +0 -3
- package/packages/telegram-notify/telegram_notify/client.py +0 -87
- package/packages/telegram-notify/telegram_notify/emojis.json +0 -22
- package/scripts/e2e-run.sh +0 -160
- package/scripts/generate-icons.js +0 -28
- package/smithery.yaml +0 -45
- package/src/__tests__/auth.test.ts +0 -147
- package/src/__tests__/brain-flow.test.ts +0 -1472
- package/src/__tests__/confidence.test.ts +0 -285
- package/src/__tests__/e2e/api-auth.test.ts +0 -182
- package/src/__tests__/e2e/api-health.test.ts +0 -103
- package/src/__tests__/e2e/cli.test.ts +0 -228
- package/src/__tests__/e2e/config.ts +0 -53
- package/src/__tests__/e2e/funnel.test.ts +0 -99
- package/src/__tests__/e2e/mcp-tools.test.ts +0 -191
- package/src/__tests__/e2e/provisioning.test.ts +0 -128
- package/src/__tests__/handlers-unit.test.ts +0 -677
- package/src/__tests__/namespace.test.ts +0 -126
- package/src/__tests__/rerank.test.ts +0 -130
- package/src/__tests__/utils.test.ts +0 -81
- package/src/auth.ts +0 -46
- package/src/bench/cachly-bench.ts +0 -217
- package/src/bench/fixtures.ts +0 -256
- package/src/ckg.ts +0 -103
- package/src/confidence.ts +0 -38
- package/src/embeddings.ts +0 -217
- package/src/handlers/advanced.ts +0 -647
- package/src/handlers/brain.ts +0 -2622
- package/src/handlers/cache.ts +0 -633
- package/src/handlers/context.ts +0 -222
- package/src/handlers/fedbrain.ts +0 -1232
- package/src/handlers/instances.ts +0 -180
- package/src/handlers/roadmap.ts +0 -201
- package/src/handlers/syndicate.ts +0 -460
- package/src/handlers/tco.ts +0 -345
- package/src/handlers/team.ts +0 -1249
- package/src/index.ts +0 -2815
- package/src/namespace.ts +0 -128
- package/src/notifier.ts +0 -5
- package/src/rerank.ts +0 -148
- package/src/search.test.ts +0 -848
- package/src/search.ts +0 -1781
- package/src/tools.ts +0 -2056
- package/src/utils.ts +0 -90
- package/test-features.cjs +0 -23
- package/tsconfig.json +0 -16
- package/vitest.config.ts +0 -14
|
@@ -1,504 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
TCO Telegram Bot
|
|
3
|
-
Commands: /start /link /unlink /trips /next /inbox /status /help
|
|
4
|
-
Forward any document/image/text → parsed via TCO AI
|
|
5
|
-
Inline keyboards for trip assignment
|
|
6
|
-
"""
|
|
7
|
-
import logging
|
|
8
|
-
import os
|
|
9
|
-
from dotenv import load_dotenv
|
|
10
|
-
import httpx
|
|
11
|
-
from telegram import (
|
|
12
|
-
Update, InlineKeyboardButton, InlineKeyboardMarkup,
|
|
13
|
-
)
|
|
14
|
-
from telegram.ext import (
|
|
15
|
-
Application, CallbackQueryHandler, CommandHandler,
|
|
16
|
-
ContextTypes, MessageHandler, filters,
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
load_dotenv()
|
|
20
|
-
TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
|
|
21
|
-
API_URL = os.environ.get("TCO_API_URL", "http://localhost:8000")
|
|
22
|
-
BOT_SECRET = os.environ.get("TCO_BOT_SECRET", TOKEN)
|
|
23
|
-
|
|
24
|
-
logging.basicConfig(level=logging.INFO)
|
|
25
|
-
log = logging.getLogger(__name__)
|
|
26
|
-
|
|
27
|
-
_ICONS = {
|
|
28
|
-
"flight": "✈️", "train": "🚂", "bus": "🚌", "hotel": "🏨",
|
|
29
|
-
"rental_car": "🚗", "activity": "🎡", "transfer": "🚕",
|
|
30
|
-
"document": "📄", "other": "📋",
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# ── Auth helpers ────────────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
def _bot_headers(user_id: str | None = None) -> dict:
|
|
37
|
-
h = {"X-Bot-Token": BOT_SECRET}
|
|
38
|
-
if user_id:
|
|
39
|
-
h["X-User-Id"] = user_id
|
|
40
|
-
return h
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
async def get_linked_user(chat_id: str) -> dict | None:
|
|
44
|
-
async with httpx.AsyncClient(timeout=10) as c:
|
|
45
|
-
r = await c.get(
|
|
46
|
-
f"{API_URL}/api/v1/users/by-telegram/{chat_id}",
|
|
47
|
-
headers=_bot_headers(),
|
|
48
|
-
)
|
|
49
|
-
if r.status_code == 200:
|
|
50
|
-
return r.json()
|
|
51
|
-
return None
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
async def _require_link(update: Update) -> dict | None:
|
|
55
|
-
"""Return linked user or send prompt and return None."""
|
|
56
|
-
user = await get_linked_user(str(update.effective_chat.id))
|
|
57
|
-
if not user:
|
|
58
|
-
await update.message.reply_text(
|
|
59
|
-
"🔗 Bitte zuerst Konto verknüpfen:\n\n"
|
|
60
|
-
"1. TCO-App → Einstellungen → *Mit Telegram verknüpfen*\n"
|
|
61
|
-
"2. Sende mir den PIN: `/link 123456`",
|
|
62
|
-
parse_mode="Markdown",
|
|
63
|
-
)
|
|
64
|
-
return user
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# ── Commands ────────────────────────────────────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
70
|
-
chat_id = str(update.effective_chat.id)
|
|
71
|
-
user = await get_linked_user(chat_id)
|
|
72
|
-
if user:
|
|
73
|
-
await update.message.reply_text(
|
|
74
|
-
f"👋 Willkommen zurück!\n\n"
|
|
75
|
-
f"Plan: *{'✦ Pro' if user.get('plan') == 'pro' else 'Free'}*\n\n"
|
|
76
|
-
f"/trips — Reisen anzeigen\n/next — Nächstes Event\n/inbox — Chaos Inbox",
|
|
77
|
-
parse_mode="Markdown",
|
|
78
|
-
)
|
|
79
|
-
else:
|
|
80
|
-
await update.message.reply_text(
|
|
81
|
-
"✈️ *Travel Chaos Organizer Bot*\n\n"
|
|
82
|
-
"Ich helfe dir, deine Reisedokumente zu verwalten.\n\n"
|
|
83
|
-
"*Erste Schritte:*\n"
|
|
84
|
-
"1. Öffne die TCO-App → Einstellungen → Telegram verknüpfen\n"
|
|
85
|
-
"2. Du bekommst einen 6-stelligen PIN\n"
|
|
86
|
-
"3. Schreib mir: `/link 123456`\n\n"
|
|
87
|
-
"Danach kannst du mir Buchungsbestätigungen, Screenshots oder PDFs schicken!",
|
|
88
|
-
parse_mode="Markdown",
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
async def cmd_help(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
93
|
-
await update.message.reply_text(
|
|
94
|
-
"*TCO Bot — Befehle:*\n\n"
|
|
95
|
-
"/trips — Deine Reisen\n"
|
|
96
|
-
"/next — Nächstes Event\n"
|
|
97
|
-
"/inbox — Chaos Inbox\n"
|
|
98
|
-
"/status — Kontostatus\n"
|
|
99
|
-
"/link \\<PIN\\> — Konto verknüpfen\n"
|
|
100
|
-
"/unlink — Verknüpfung aufheben\n"
|
|
101
|
-
"/help — Diese Hilfe\n\n"
|
|
102
|
-
"*Dokumente senden:*\n"
|
|
103
|
-
"Einfach Buchungsbestätigung, PDF oder Screenshot schicken — wird automatisch mit KI geparst ✨\n\n"
|
|
104
|
-
"*Text senden:*\n"
|
|
105
|
-
"Buchungsbestätigung als Text einfügen und senden.",
|
|
106
|
-
parse_mode="Markdown",
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
async def cmd_link(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
111
|
-
if not ctx.args:
|
|
112
|
-
await update.message.reply_text("Bitte PIN angeben: `/link 123456`", parse_mode="Markdown")
|
|
113
|
-
return
|
|
114
|
-
pin = ctx.args[0].strip()
|
|
115
|
-
chat_id = str(update.effective_chat.id)
|
|
116
|
-
async with httpx.AsyncClient(timeout=10) as c:
|
|
117
|
-
r = await c.post(
|
|
118
|
-
f"{API_URL}/api/v1/users/telegram-link",
|
|
119
|
-
params={"pin": pin, "chat_id": chat_id},
|
|
120
|
-
)
|
|
121
|
-
if r.status_code == 200:
|
|
122
|
-
await update.message.reply_text(
|
|
123
|
-
"✅ *Konto erfolgreich verknüpft!*\n\n"
|
|
124
|
-
"Du kannst jetzt:\n"
|
|
125
|
-
"• Buchungsbestätigungen oder PDFs schicken → werden automatisch geparst\n"
|
|
126
|
-
"• /trips — Reisen anzeigen\n"
|
|
127
|
-
"• /inbox — Chaos Inbox verwalten\n"
|
|
128
|
-
"• /next — Nächstes Event anzeigen",
|
|
129
|
-
parse_mode="Markdown",
|
|
130
|
-
)
|
|
131
|
-
elif r.status_code == 410:
|
|
132
|
-
await update.message.reply_text("⏰ PIN abgelaufen. Bitte neuen PIN in der App generieren.")
|
|
133
|
-
elif r.status_code == 404:
|
|
134
|
-
await update.message.reply_text("❌ PIN nicht gefunden. Bitte prüfe die Eingabe.")
|
|
135
|
-
else:
|
|
136
|
-
await update.message.reply_text("⚠️ Fehler beim Verknüpfen. Bitte versuche es erneut.")
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
async def cmd_unlink(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
140
|
-
user = await _require_link(update)
|
|
141
|
-
if not user:
|
|
142
|
-
return
|
|
143
|
-
keyboard = InlineKeyboardMarkup([[
|
|
144
|
-
InlineKeyboardButton("✅ Ja, trennen", callback_data="unlink:confirm"),
|
|
145
|
-
InlineKeyboardButton("❌ Abbrechen", callback_data="unlink:cancel"),
|
|
146
|
-
]])
|
|
147
|
-
await update.message.reply_text(
|
|
148
|
-
"⚠️ Telegram-Verknüpfung aufheben?\n\nDu erhältst dann keine Bot-Nachrichten mehr.",
|
|
149
|
-
reply_markup=keyboard,
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
async def cmd_status(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
154
|
-
chat_id = str(update.effective_chat.id)
|
|
155
|
-
user = await get_linked_user(chat_id)
|
|
156
|
-
if not user:
|
|
157
|
-
await update.message.reply_text(
|
|
158
|
-
"🔴 Konto *nicht verknüpft*\n\nNutze `/link <PIN>` zum Verbinden.",
|
|
159
|
-
parse_mode="Markdown",
|
|
160
|
-
)
|
|
161
|
-
return
|
|
162
|
-
plan = user.get("plan", "free")
|
|
163
|
-
plan_label = "✦ Pro" if plan == "pro" else "Free"
|
|
164
|
-
await update.message.reply_text(
|
|
165
|
-
f"✅ *Konto verknüpft*\n\nPlan: *{plan_label}*\n\nTelegramm-Chat-ID: `{chat_id}`",
|
|
166
|
-
parse_mode="Markdown",
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
async def cmd_trips(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
171
|
-
user = await _require_link(update)
|
|
172
|
-
if not user:
|
|
173
|
-
return
|
|
174
|
-
async with httpx.AsyncClient(timeout=10) as c:
|
|
175
|
-
r = await c.get(f"{API_URL}/api/v1/trips", headers=_bot_headers(user["id"]))
|
|
176
|
-
if r.status_code != 200:
|
|
177
|
-
await update.message.reply_text("⚠️ Fehler beim Laden der Trips.")
|
|
178
|
-
return
|
|
179
|
-
trips = r.json()
|
|
180
|
-
if not trips:
|
|
181
|
-
await update.message.reply_text("📭 Noch keine Trips. Erstelle deinen ersten Trip in der TCO-App!")
|
|
182
|
-
return
|
|
183
|
-
# Build inline keyboard — each trip is a button to view its timeline
|
|
184
|
-
buttons = [
|
|
185
|
-
[InlineKeyboardButton(
|
|
186
|
-
f"{'✈️ ' if t.get('start_date') else '📋 '}{t['name']}",
|
|
187
|
-
callback_data=f"trip:{t['id']}:{t['name'][:20]}",
|
|
188
|
-
)]
|
|
189
|
-
for t in trips[:8]
|
|
190
|
-
]
|
|
191
|
-
await update.message.reply_text(
|
|
192
|
-
f"🗺️ *Deine Trips ({len(trips)}):*",
|
|
193
|
-
reply_markup=InlineKeyboardMarkup(buttons),
|
|
194
|
-
parse_mode="Markdown",
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
async def cmd_next(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
199
|
-
user = await _require_link(update)
|
|
200
|
-
if not user:
|
|
201
|
-
return
|
|
202
|
-
async with httpx.AsyncClient(timeout=10) as c:
|
|
203
|
-
r = await c.get(f"{API_URL}/api/v1/trips", headers=_bot_headers(user["id"]))
|
|
204
|
-
if r.status_code != 200 or not r.json():
|
|
205
|
-
await update.message.reply_text("Keine Trips gefunden.")
|
|
206
|
-
return
|
|
207
|
-
from datetime import datetime, timezone
|
|
208
|
-
today = datetime.now(timezone.utc).date().isoformat()
|
|
209
|
-
upcoming = [t for t in r.json() if t.get("start_date") and t["start_date"] >= today]
|
|
210
|
-
if not upcoming:
|
|
211
|
-
await update.message.reply_text("📭 Kein bevorstehender Trip gefunden.")
|
|
212
|
-
return
|
|
213
|
-
t = upcoming[0]
|
|
214
|
-
start = t.get("start_date", "?")[:10]
|
|
215
|
-
end = t.get("end_date", "?")[:10] if t.get("end_date") else "?"
|
|
216
|
-
days = ""
|
|
217
|
-
try:
|
|
218
|
-
from datetime import date
|
|
219
|
-
d = (date.fromisoformat(start) - date.today()).days
|
|
220
|
-
days = f" (in {d} {'Tag' if d == 1 else 'Tagen'})"
|
|
221
|
-
except Exception:
|
|
222
|
-
pass
|
|
223
|
-
keyboard = InlineKeyboardMarkup([[
|
|
224
|
-
InlineKeyboardButton("📅 Timeline anzeigen", callback_data=f"trip:{t['id']}:{t['name'][:20]}"),
|
|
225
|
-
]])
|
|
226
|
-
await update.message.reply_text(
|
|
227
|
-
f"🗓️ *Nächster Trip:*\n\n*{t['name']}*\nStart: {start}{days}\nEnde: {end}",
|
|
228
|
-
reply_markup=keyboard,
|
|
229
|
-
parse_mode="Markdown",
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
async def cmd_inbox(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
234
|
-
user = await _require_link(update)
|
|
235
|
-
if not user:
|
|
236
|
-
return
|
|
237
|
-
async with httpx.AsyncClient(timeout=10) as c:
|
|
238
|
-
r = await c.get(
|
|
239
|
-
f"{API_URL}/api/v1/inbox?status_filter=pending",
|
|
240
|
-
headers=_bot_headers(user["id"]),
|
|
241
|
-
)
|
|
242
|
-
if r.status_code != 200:
|
|
243
|
-
await update.message.reply_text("⚠️ Fehler beim Laden des Inbox.")
|
|
244
|
-
return
|
|
245
|
-
items = r.json()
|
|
246
|
-
if not items:
|
|
247
|
-
await update.message.reply_text("🎉 Chaos Inbox ist leer!")
|
|
248
|
-
return
|
|
249
|
-
# Show first 5 items with assign/reject buttons
|
|
250
|
-
for item in items[:5]:
|
|
251
|
-
pd = item.get("parsed_data") or {}
|
|
252
|
-
title = pd.get("title") or "(kein Titel)"
|
|
253
|
-
itype = pd.get("type") or "other"
|
|
254
|
-
icon = _ICONS.get(itype, "📋")
|
|
255
|
-
provider = pd.get("provider") or ""
|
|
256
|
-
ref = pd.get("booking_ref") or ""
|
|
257
|
-
lines = [f"{icon} *{title}*"]
|
|
258
|
-
if provider:
|
|
259
|
-
lines.append(f"Anbieter: {provider}")
|
|
260
|
-
if ref:
|
|
261
|
-
lines.append(f"Ref: `{ref}`")
|
|
262
|
-
keyboard = InlineKeyboardMarkup([[
|
|
263
|
-
InlineKeyboardButton("✈️ Trip zuweisen", callback_data=f"assign:{item['id']}:pick"),
|
|
264
|
-
InlineKeyboardButton("🗑️ Löschen", callback_data=f"reject:{item['id']}"),
|
|
265
|
-
]])
|
|
266
|
-
await update.message.reply_text(
|
|
267
|
-
"\n".join(lines),
|
|
268
|
-
reply_markup=keyboard,
|
|
269
|
-
parse_mode="Markdown",
|
|
270
|
-
)
|
|
271
|
-
if len(items) > 5:
|
|
272
|
-
await update.message.reply_text(f"… und {len(items) - 5} weitere. Öffne die App für die vollständige Ansicht.")
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
# ── Callback query handlers ─────────────────────────────────────────────────────
|
|
276
|
-
|
|
277
|
-
async def handle_callback(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
278
|
-
query = update.callback_query
|
|
279
|
-
await query.answer()
|
|
280
|
-
data = query.data or ""
|
|
281
|
-
chat_id = str(update.effective_chat.id)
|
|
282
|
-
user = await get_linked_user(chat_id)
|
|
283
|
-
if not user:
|
|
284
|
-
await query.edit_message_text("⚠️ Konto nicht verknüpft. Nutze /link.")
|
|
285
|
-
return
|
|
286
|
-
|
|
287
|
-
# ── unlink confirm/cancel ──────────────────────────────────────────────
|
|
288
|
-
if data == "unlink:confirm":
|
|
289
|
-
async with httpx.AsyncClient(timeout=10) as c:
|
|
290
|
-
await c.delete(
|
|
291
|
-
f"{API_URL}/api/v1/users/telegram-unlink",
|
|
292
|
-
headers={"Authorization": f"X-User-Id {user['id']}", **_bot_headers(user["id"])},
|
|
293
|
-
)
|
|
294
|
-
await query.edit_message_text("✅ Verknüpfung aufgehoben. Auf Wiedersehen!")
|
|
295
|
-
return
|
|
296
|
-
if data == "unlink:cancel":
|
|
297
|
-
await query.edit_message_text("❌ Abgebrochen. Verknüpfung bleibt bestehen.")
|
|
298
|
-
return
|
|
299
|
-
|
|
300
|
-
# ── show trip timeline ─────────────────────────────────────────────────
|
|
301
|
-
if data.startswith("trip:"):
|
|
302
|
-
parts = data.split(":", 2)
|
|
303
|
-
trip_id = parts[1]
|
|
304
|
-
trip_name = parts[2] if len(parts) > 2 else "Trip"
|
|
305
|
-
async with httpx.AsyncClient(timeout=10) as c:
|
|
306
|
-
r = await c.get(f"{API_URL}/api/v1/trips/{trip_id}/items", headers=_bot_headers(user["id"]))
|
|
307
|
-
if r.status_code != 200 or not r.json():
|
|
308
|
-
await query.edit_message_text(f"📂 *{trip_name}* hat noch keine Einträge.", parse_mode="Markdown")
|
|
309
|
-
return
|
|
310
|
-
items = r.json()
|
|
311
|
-
lines = [f"📅 *{trip_name}* — {len(items)} Einträge\n"]
|
|
312
|
-
for i in items[:10]:
|
|
313
|
-
icon = _ICONS.get(i.get("type", "other"), "📋")
|
|
314
|
-
time = i.get("event_at", "")[:16].replace("T", " ") if i.get("event_at") else ""
|
|
315
|
-
lines.append(f"{icon} *{i['title']}*{(' · ' + time) if time else ''}")
|
|
316
|
-
if len(items) > 10:
|
|
317
|
-
lines.append(f"… +{len(items) - 10} weitere")
|
|
318
|
-
await query.edit_message_text("\n".join(lines), parse_mode="Markdown")
|
|
319
|
-
return
|
|
320
|
-
|
|
321
|
-
# ── assign inbox item — pick trip ──────────────────────────────────────
|
|
322
|
-
if data.startswith("assign:") and data.endswith(":pick"):
|
|
323
|
-
inbox_id = data.split(":")[1]
|
|
324
|
-
async with httpx.AsyncClient(timeout=10) as c:
|
|
325
|
-
r = await c.get(f"{API_URL}/api/v1/trips", headers=_bot_headers(user["id"]))
|
|
326
|
-
trips = r.json() if r.status_code == 200 else []
|
|
327
|
-
if not trips:
|
|
328
|
-
await query.edit_message_text("📭 Keine Trips vorhanden. Erstelle zuerst einen Trip in der App.")
|
|
329
|
-
return
|
|
330
|
-
buttons = [
|
|
331
|
-
[InlineKeyboardButton(t["name"], callback_data=f"assign:{inbox_id}:{t['id']}")]
|
|
332
|
-
for t in trips[:8]
|
|
333
|
-
]
|
|
334
|
-
buttons.append([InlineKeyboardButton("❌ Abbrechen", callback_data=f"assign:{inbox_id}:cancel")])
|
|
335
|
-
await query.edit_message_reply_markup(InlineKeyboardMarkup(buttons))
|
|
336
|
-
return
|
|
337
|
-
|
|
338
|
-
if data.startswith("assign:") and data.endswith(":cancel"):
|
|
339
|
-
await query.edit_message_reply_markup(None)
|
|
340
|
-
return
|
|
341
|
-
|
|
342
|
-
# ── assign inbox item to specific trip ─────────────────────────────────
|
|
343
|
-
if data.startswith("assign:"):
|
|
344
|
-
parts = data.split(":", 2)
|
|
345
|
-
inbox_id, trip_id = parts[1], parts[2]
|
|
346
|
-
if trip_id in ("pick", "cancel"):
|
|
347
|
-
return
|
|
348
|
-
async with httpx.AsyncClient(timeout=10) as c:
|
|
349
|
-
r = await c.post(
|
|
350
|
-
f"{API_URL}/api/v1/inbox/{inbox_id}/assign",
|
|
351
|
-
json={"trip_id": trip_id, "type": "other"},
|
|
352
|
-
headers=_bot_headers(user["id"]),
|
|
353
|
-
)
|
|
354
|
-
if r.status_code == 200:
|
|
355
|
-
await query.edit_message_text("✅ Dem Trip zugewiesen!", parse_mode="Markdown")
|
|
356
|
-
else:
|
|
357
|
-
await query.edit_message_text(f"⚠️ Fehler beim Zuweisen ({r.status_code}).")
|
|
358
|
-
return
|
|
359
|
-
|
|
360
|
-
# ── reject inbox item ──────────────────────────────────────────────────
|
|
361
|
-
if data.startswith("reject:"):
|
|
362
|
-
inbox_id = data.split(":")[1]
|
|
363
|
-
async with httpx.AsyncClient(timeout=10) as c:
|
|
364
|
-
await c.delete(f"{API_URL}/api/v1/inbox/{inbox_id}", headers=_bot_headers(user["id"]))
|
|
365
|
-
await query.edit_message_text("🗑️ Gelöscht.")
|
|
366
|
-
return
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
# ── Message handlers ────────────────────────────────────────────────────────────
|
|
370
|
-
|
|
371
|
-
async def handle_document(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
372
|
-
user = await _require_link(update)
|
|
373
|
-
if not user:
|
|
374
|
-
return
|
|
375
|
-
msg = await update.message.reply_text("⏳ Analysiere Dokument…")
|
|
376
|
-
doc = update.message.document
|
|
377
|
-
photo = update.message.photo
|
|
378
|
-
try:
|
|
379
|
-
if photo:
|
|
380
|
-
file = await ctx.bot.get_file(photo[-1].file_id)
|
|
381
|
-
mime, fname = "image/jpeg", "photo.jpg"
|
|
382
|
-
elif doc:
|
|
383
|
-
file = await ctx.bot.get_file(doc.file_id)
|
|
384
|
-
mime = doc.mime_type or "application/octet-stream"
|
|
385
|
-
fname = doc.file_name or "document"
|
|
386
|
-
else:
|
|
387
|
-
await msg.edit_text("⚠️ Kein Dokument erkannt.")
|
|
388
|
-
return
|
|
389
|
-
file_bytes = await file.download_as_bytearray()
|
|
390
|
-
async with httpx.AsyncClient(timeout=60) as c:
|
|
391
|
-
r = await c.post(
|
|
392
|
-
f"{API_URL}/api/v1/parse/file",
|
|
393
|
-
files={"file": (fname, bytes(file_bytes), mime)},
|
|
394
|
-
headers=_bot_headers(user["id"]),
|
|
395
|
-
)
|
|
396
|
-
if r.status_code == 200:
|
|
397
|
-
parsed = r.json().get("parsed", {})
|
|
398
|
-
title = parsed.get("title") or "Dokument"
|
|
399
|
-
itype = parsed.get("type") or "other"
|
|
400
|
-
icon = _ICONS.get(itype, "📋")
|
|
401
|
-
ref = parsed.get("booking_ref") or ""
|
|
402
|
-
provider = parsed.get("provider") or ""
|
|
403
|
-
event_at = parsed.get("event_at") or ""
|
|
404
|
-
lines = [f"{icon} *{title}*", f"Typ: {itype}"]
|
|
405
|
-
if provider:
|
|
406
|
-
lines.append(f"Anbieter: {provider}")
|
|
407
|
-
if ref:
|
|
408
|
-
lines.append(f"Ref: `{ref}`")
|
|
409
|
-
if event_at:
|
|
410
|
-
lines.append(f"Datum: {event_at[:16].replace('T', ' ')}")
|
|
411
|
-
lines.append("\n📥 Im Chaos Inbox")
|
|
412
|
-
# Fetch trips for inline assignment
|
|
413
|
-
async with httpx.AsyncClient(timeout=10) as c2:
|
|
414
|
-
trips_r = await c2.get(f"{API_URL}/api/v1/trips", headers=_bot_headers(user["id"]))
|
|
415
|
-
inbox_id = r.json().get("inbox_id")
|
|
416
|
-
keyboard = None
|
|
417
|
-
if inbox_id and trips_r.status_code == 200 and trips_r.json():
|
|
418
|
-
trips = trips_r.json()[:6]
|
|
419
|
-
buttons = [
|
|
420
|
-
[InlineKeyboardButton(f"➡️ {t['name']}", callback_data=f"assign:{inbox_id}:{t['id']}")]
|
|
421
|
-
for t in trips
|
|
422
|
-
]
|
|
423
|
-
buttons.append([InlineKeyboardButton("🗑️ Ablehnen", callback_data=f"reject:{inbox_id}")])
|
|
424
|
-
keyboard = InlineKeyboardMarkup(buttons)
|
|
425
|
-
await msg.edit_text(
|
|
426
|
-
"\n".join(lines),
|
|
427
|
-
reply_markup=keyboard,
|
|
428
|
-
parse_mode="Markdown",
|
|
429
|
-
)
|
|
430
|
-
elif r.status_code == 429:
|
|
431
|
-
await msg.edit_text("⚠️ Tageslimit erreicht. Morgen wieder verfügbar (oder auf Pro upgraden).")
|
|
432
|
-
else:
|
|
433
|
-
await msg.edit_text(f"⚠️ Fehler beim Parsen ({r.status_code}). Prüfe Ollama.")
|
|
434
|
-
except Exception:
|
|
435
|
-
log.exception("handle_document error")
|
|
436
|
-
await msg.edit_text("⚠️ Technischer Fehler. Bitte versuche es erneut.")
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
async def handle_text(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
440
|
-
text = update.message.text or ""
|
|
441
|
-
if text.startswith("/"):
|
|
442
|
-
return
|
|
443
|
-
user = await _require_link(update)
|
|
444
|
-
if not user:
|
|
445
|
-
return
|
|
446
|
-
if len(text) < 20:
|
|
447
|
-
await update.message.reply_text("Text zu kurz zum Parsen. Bitte Buchungsbestätigung oder E-Mail einfügen.")
|
|
448
|
-
return
|
|
449
|
-
msg = await update.message.reply_text("⏳ Analysiere Text…")
|
|
450
|
-
async with httpx.AsyncClient(timeout=60) as c:
|
|
451
|
-
r = await c.post(
|
|
452
|
-
f"{API_URL}/api/v1/parse/text",
|
|
453
|
-
data={"raw_text": text},
|
|
454
|
-
headers=_bot_headers(user["id"]),
|
|
455
|
-
)
|
|
456
|
-
if r.status_code == 200:
|
|
457
|
-
parsed = r.json().get("parsed", {})
|
|
458
|
-
title = parsed.get("title") or "Text"
|
|
459
|
-
itype = parsed.get("type") or "other"
|
|
460
|
-
icon = _ICONS.get(itype, "📋")
|
|
461
|
-
inbox_id = r.json().get("inbox_id")
|
|
462
|
-
# Build inline keyboard for assignment
|
|
463
|
-
keyboard = None
|
|
464
|
-
if inbox_id:
|
|
465
|
-
async with httpx.AsyncClient(timeout=10) as c2:
|
|
466
|
-
trips_r = await c2.get(f"{API_URL}/api/v1/trips", headers=_bot_headers(user["id"]))
|
|
467
|
-
if trips_r.status_code == 200 and trips_r.json():
|
|
468
|
-
trips = trips_r.json()[:6]
|
|
469
|
-
buttons = [
|
|
470
|
-
[InlineKeyboardButton(f"➡️ {t['name']}", callback_data=f"assign:{inbox_id}:{t['id']}")]
|
|
471
|
-
for t in trips
|
|
472
|
-
]
|
|
473
|
-
buttons.append([InlineKeyboardButton("🗑️ Ablehnen", callback_data=f"reject:{inbox_id}")])
|
|
474
|
-
keyboard = InlineKeyboardMarkup(buttons)
|
|
475
|
-
await msg.edit_text(
|
|
476
|
-
f"{icon} *{title}* erkannt und im Inbox gespeichert.",
|
|
477
|
-
reply_markup=keyboard,
|
|
478
|
-
parse_mode="Markdown",
|
|
479
|
-
)
|
|
480
|
-
elif r.status_code == 429:
|
|
481
|
-
await msg.edit_text("⚠️ Tageslimit erreicht.")
|
|
482
|
-
else:
|
|
483
|
-
await msg.edit_text("⚠️ Konnte Text nicht parsen.")
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
def main():
|
|
487
|
-
app = Application.builder().token(TOKEN).build()
|
|
488
|
-
app.add_handler(CommandHandler("start", cmd_start))
|
|
489
|
-
app.add_handler(CommandHandler("help", cmd_help))
|
|
490
|
-
app.add_handler(CommandHandler("link", cmd_link))
|
|
491
|
-
app.add_handler(CommandHandler("unlink", cmd_unlink))
|
|
492
|
-
app.add_handler(CommandHandler("status", cmd_status))
|
|
493
|
-
app.add_handler(CommandHandler("trips", cmd_trips))
|
|
494
|
-
app.add_handler(CommandHandler("next", cmd_next))
|
|
495
|
-
app.add_handler(CommandHandler("inbox", cmd_inbox))
|
|
496
|
-
app.add_handler(CallbackQueryHandler(handle_callback))
|
|
497
|
-
app.add_handler(MessageHandler(filters.Document.ALL | filters.PHOTO, handle_document))
|
|
498
|
-
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text))
|
|
499
|
-
log.info("TCO Bot starting…")
|
|
500
|
-
app.run_polling(drop_pending_updates=True)
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if __name__ == "__main__":
|
|
504
|
-
main()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
-
import { jwtExpiryMs, checkJwt, handleApiError } from '../auth.js';
|
|
4
|
-
function makeJwt(payload) {
|
|
5
|
-
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
|
|
6
|
-
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
7
|
-
return `${header}.${body}.fakesig`;
|
|
8
|
-
}
|
|
9
|
-
const nowSec = () => Math.floor(Date.now() / 1000);
|
|
10
|
-
describe('jwtExpiryMs', () => {
|
|
11
|
-
it('returns null for an empty string', () => {
|
|
12
|
-
expect(jwtExpiryMs('')).toBeNull();
|
|
13
|
-
});
|
|
14
|
-
it('returns null for a plain (non-JWT) string', () => {
|
|
15
|
-
expect(jwtExpiryMs('not-a-token')).toBeNull();
|
|
16
|
-
});
|
|
17
|
-
it('returns null when the JWT has only two parts', () => {
|
|
18
|
-
expect(jwtExpiryMs('header.payload')).toBeNull();
|
|
19
|
-
});
|
|
20
|
-
it('returns null when the JWT has four parts', () => {
|
|
21
|
-
expect(jwtExpiryMs('a.b.c.d')).toBeNull();
|
|
22
|
-
});
|
|
23
|
-
it('returns null when the payload base64 is invalid', () => {
|
|
24
|
-
expect(jwtExpiryMs('header.!!!.sig')).toBeNull();
|
|
25
|
-
});
|
|
26
|
-
it('returns null when the payload has no exp field', () => {
|
|
27
|
-
expect(jwtExpiryMs(makeJwt({ sub: 'user-123', iat: nowSec() }))).toBeNull();
|
|
28
|
-
});
|
|
29
|
-
it('returns null when exp is 0', () => {
|
|
30
|
-
expect(jwtExpiryMs(makeJwt({ exp: 0 }))).toBeNull();
|
|
31
|
-
});
|
|
32
|
-
it('converts exp (seconds) to milliseconds', () => {
|
|
33
|
-
const expSec = nowSec() + 3600;
|
|
34
|
-
expect(jwtExpiryMs(makeJwt({ exp: expSec }))).toBe(expSec * 1000);
|
|
35
|
-
});
|
|
36
|
-
it('returns a past timestamp for an expired token', () => {
|
|
37
|
-
expect(jwtExpiryMs(makeJwt({ exp: nowSec() - 300 }))).toBeLessThan(Date.now());
|
|
38
|
-
});
|
|
39
|
-
it('returns a future timestamp for a valid token', () => {
|
|
40
|
-
expect(jwtExpiryMs(makeJwt({ exp: nowSec() + 86400 }))).toBeGreaterThan(Date.now());
|
|
41
|
-
});
|
|
42
|
-
it('handles a token expiring in less than 60 minutes', () => {
|
|
43
|
-
const expSec = nowSec() + 30 * 60;
|
|
44
|
-
const result = jwtExpiryMs(makeJwt({ exp: expSec }));
|
|
45
|
-
const minsLeft = Math.floor((result - Date.now()) / 60_000);
|
|
46
|
-
expect(minsLeft).toBeGreaterThanOrEqual(29);
|
|
47
|
-
expect(minsLeft).toBeLessThanOrEqual(30);
|
|
48
|
-
});
|
|
49
|
-
it('ignores extra claims', () => {
|
|
50
|
-
const expSec = nowSec() + 3600;
|
|
51
|
-
expect(jwtExpiryMs(makeJwt({ sub: 'u', iat: nowSec(), exp: expSec, plan: 'free' }))).toBe(expSec * 1000);
|
|
52
|
-
});
|
|
53
|
-
it('handles padding-free base64url payloads', () => {
|
|
54
|
-
// base64url (no =) — Buffer.from handles this natively
|
|
55
|
-
const expSec = nowSec() + 7200;
|
|
56
|
-
expect(jwtExpiryMs(makeJwt({ exp: expSec, x: 'a' }))).toBe(expSec * 1000);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
describe('checkJwt', () => {
|
|
60
|
-
it('throws InvalidRequest when jwt is empty', () => {
|
|
61
|
-
expect(() => checkJwt('')).toThrowError(expect.objectContaining({ code: ErrorCode.InvalidRequest, message: expect.stringContaining('CACHLY_JWT env var not set') }));
|
|
62
|
-
});
|
|
63
|
-
it('throws with setup URL when jwt is missing', () => {
|
|
64
|
-
expect(() => checkJwt('')).toThrowError(expect.objectContaining({ message: expect.stringContaining('cachly.dev/setup-ai') }));
|
|
65
|
-
});
|
|
66
|
-
it('throws InvalidRequest with expiry timestamp for an expired token', () => {
|
|
67
|
-
const expSec = nowSec() - 60;
|
|
68
|
-
const token = makeJwt({ exp: expSec });
|
|
69
|
-
expect(() => checkJwt(token)).toThrowError(expect.objectContaining({
|
|
70
|
-
code: ErrorCode.InvalidRequest,
|
|
71
|
-
message: expect.stringContaining('expired at'),
|
|
72
|
-
}));
|
|
73
|
-
});
|
|
74
|
-
it('does not throw for a valid non-expired token', () => {
|
|
75
|
-
expect(() => checkJwt(makeJwt({ exp: nowSec() + 3600 }))).not.toThrow();
|
|
76
|
-
});
|
|
77
|
-
it('does not throw for a token with no exp claim', () => {
|
|
78
|
-
expect(() => checkJwt(makeJwt({ sub: 'user' }))).not.toThrow();
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
describe('handleApiError', () => {
|
|
82
|
-
it('throws InvalidRequest for 401 with the detail message', () => {
|
|
83
|
-
expect(() => handleApiError(401, 'token expired')).toThrowError(expect.objectContaining({
|
|
84
|
-
code: ErrorCode.InvalidRequest,
|
|
85
|
-
message: expect.stringContaining('Authentication failed (401): token expired'),
|
|
86
|
-
}));
|
|
87
|
-
});
|
|
88
|
-
it('throws InvalidRequest for 403 with the detail message', () => {
|
|
89
|
-
expect(() => handleApiError(403, 'plan limit reached')).toThrowError(expect.objectContaining({
|
|
90
|
-
code: ErrorCode.InvalidRequest,
|
|
91
|
-
message: expect.stringContaining('Access denied (403): plan limit reached'),
|
|
92
|
-
}));
|
|
93
|
-
});
|
|
94
|
-
it('throws InternalError for other status codes', () => {
|
|
95
|
-
expect(() => handleApiError(500, 'server crash')).toThrowError(expect.objectContaining({
|
|
96
|
-
code: ErrorCode.InternalError,
|
|
97
|
-
message: expect.stringContaining('cachly API error 500: server crash'),
|
|
98
|
-
}));
|
|
99
|
-
});
|
|
100
|
-
it('includes the cachly.dev/setup-ai link for 401 errors', () => {
|
|
101
|
-
expect(() => handleApiError(401, 'Unauthorized')).toThrowError(expect.objectContaining({ message: expect.stringContaining('cachly.dev/setup-ai') }));
|
|
102
|
-
});
|
|
103
|
-
it('includes account access hint for 403 errors', () => {
|
|
104
|
-
expect(() => handleApiError(403, 'Forbidden')).toThrowError(expect.objectContaining({ message: expect.stringContaining('access to this resource') }));
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
//# sourceMappingURL=auth.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"auth.test.js","sourceRoot":"","sources":["../../../src/__tests__/auth.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Use-case tests for the Cachly Brain flow.
|
|
3
|
-
*
|
|
4
|
-
* Tests the core Brain operations end-to-end using an in-memory Redis mock:
|
|
5
|
-
* learn_from_attempts → CKG update → recall_best_solution (BM25)
|
|
6
|
-
* session_start → lessons summary
|
|
7
|
-
* CKG: Bayesian confidence, contradiction detection
|
|
8
|
-
* Embeddings: provider detection, config sync
|
|
9
|
-
*
|
|
10
|
-
* Run: npx vitest run src/__tests__/brain-flow.test.ts
|
|
11
|
-
*/
|
|
12
|
-
export {};
|